@trenchwork/coder 1.3.0

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 (560) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +173 -0
  3. package/agents/trenchwork-code.rules.json +199 -0
  4. package/dist/bin/deepseek.d.ts +3 -0
  5. package/dist/bin/deepseek.d.ts.map +1 -0
  6. package/dist/bin/deepseek.js +23 -0
  7. package/dist/bin/deepseek.js.map +1 -0
  8. package/dist/capabilities/baseCapability.d.ts +72 -0
  9. package/dist/capabilities/baseCapability.d.ts.map +1 -0
  10. package/dist/capabilities/baseCapability.js +183 -0
  11. package/dist/capabilities/baseCapability.js.map +1 -0
  12. package/dist/capabilities/bashCapability.d.ts +13 -0
  13. package/dist/capabilities/bashCapability.d.ts.map +1 -0
  14. package/dist/capabilities/bashCapability.js +24 -0
  15. package/dist/capabilities/bashCapability.js.map +1 -0
  16. package/dist/capabilities/editCapability.d.ts +17 -0
  17. package/dist/capabilities/editCapability.d.ts.map +1 -0
  18. package/dist/capabilities/editCapability.js +27 -0
  19. package/dist/capabilities/editCapability.js.map +1 -0
  20. package/dist/capabilities/enhancedGitCapability.d.ts +7 -0
  21. package/dist/capabilities/enhancedGitCapability.d.ts.map +1 -0
  22. package/dist/capabilities/enhancedGitCapability.js +220 -0
  23. package/dist/capabilities/enhancedGitCapability.js.map +1 -0
  24. package/dist/capabilities/filesystemCapability.d.ts +13 -0
  25. package/dist/capabilities/filesystemCapability.d.ts.map +1 -0
  26. package/dist/capabilities/filesystemCapability.js +24 -0
  27. package/dist/capabilities/filesystemCapability.js.map +1 -0
  28. package/dist/capabilities/gitHistoryCapability.d.ts +6 -0
  29. package/dist/capabilities/gitHistoryCapability.d.ts.map +1 -0
  30. package/dist/capabilities/gitHistoryCapability.js +184 -0
  31. package/dist/capabilities/gitHistoryCapability.js.map +1 -0
  32. package/dist/capabilities/hitlCapability.d.ts +18 -0
  33. package/dist/capabilities/hitlCapability.d.ts.map +1 -0
  34. package/dist/capabilities/hitlCapability.js +29 -0
  35. package/dist/capabilities/hitlCapability.js.map +1 -0
  36. package/dist/capabilities/index.d.ts +11 -0
  37. package/dist/capabilities/index.d.ts.map +1 -0
  38. package/dist/capabilities/index.js +16 -0
  39. package/dist/capabilities/index.js.map +1 -0
  40. package/dist/capabilities/memoryCapability.d.ts +10 -0
  41. package/dist/capabilities/memoryCapability.d.ts.map +1 -0
  42. package/dist/capabilities/memoryCapability.js +22 -0
  43. package/dist/capabilities/memoryCapability.js.map +1 -0
  44. package/dist/capabilities/notebookCapability.d.ts +6 -0
  45. package/dist/capabilities/notebookCapability.d.ts.map +1 -0
  46. package/dist/capabilities/notebookCapability.js +17 -0
  47. package/dist/capabilities/notebookCapability.js.map +1 -0
  48. package/dist/capabilities/searchCapability.d.ts +19 -0
  49. package/dist/capabilities/searchCapability.d.ts.map +1 -0
  50. package/dist/capabilities/searchCapability.js +29 -0
  51. package/dist/capabilities/searchCapability.js.map +1 -0
  52. package/dist/capabilities/skillCapability.d.ts +6 -0
  53. package/dist/capabilities/skillCapability.d.ts.map +1 -0
  54. package/dist/capabilities/skillCapability.js +17 -0
  55. package/dist/capabilities/skillCapability.js.map +1 -0
  56. package/dist/capabilities/todoCapability.d.ts +11 -0
  57. package/dist/capabilities/todoCapability.d.ts.map +1 -0
  58. package/dist/capabilities/todoCapability.js +22 -0
  59. package/dist/capabilities/todoCapability.js.map +1 -0
  60. package/dist/capabilities/toolManifest.d.ts +3 -0
  61. package/dist/capabilities/toolManifest.d.ts.map +1 -0
  62. package/dist/capabilities/toolManifest.js +163 -0
  63. package/dist/capabilities/toolManifest.js.map +1 -0
  64. package/dist/capabilities/toolRegistry.d.ts +25 -0
  65. package/dist/capabilities/toolRegistry.d.ts.map +1 -0
  66. package/dist/capabilities/toolRegistry.js +150 -0
  67. package/dist/capabilities/toolRegistry.js.map +1 -0
  68. package/dist/capabilities/unifiedCodingCapability.d.ts +62 -0
  69. package/dist/capabilities/unifiedCodingCapability.d.ts.map +1 -0
  70. package/dist/capabilities/unifiedCodingCapability.js +790 -0
  71. package/dist/capabilities/unifiedCodingCapability.js.map +1 -0
  72. package/dist/capabilities/webCapability.d.ts +23 -0
  73. package/dist/capabilities/webCapability.d.ts.map +1 -0
  74. package/dist/capabilities/webCapability.js +33 -0
  75. package/dist/capabilities/webCapability.js.map +1 -0
  76. package/dist/config.d.ts +25 -0
  77. package/dist/config.d.ts.map +1 -0
  78. package/dist/config.js +181 -0
  79. package/dist/config.js.map +1 -0
  80. package/dist/contracts/agent-profiles.schema.json +25 -0
  81. package/dist/contracts/agent-schemas.json +158 -0
  82. package/dist/contracts/models.schema.json +9 -0
  83. package/dist/contracts/module-schema.json +367 -0
  84. package/dist/contracts/schemas/agent-profile.schema.json +157 -0
  85. package/dist/contracts/schemas/agent-rules.schema.json +238 -0
  86. package/dist/contracts/schemas/agent-schemas.schema.json +528 -0
  87. package/dist/contracts/schemas/agent.schema.json +90 -0
  88. package/dist/contracts/schemas/tool-selection.schema.json +174 -0
  89. package/dist/contracts/tools.schema.json +42 -0
  90. package/dist/contracts/unified-schema.json +40 -0
  91. package/dist/contracts/v1/agent.d.ts +225 -0
  92. package/dist/contracts/v1/agent.d.ts.map +1 -0
  93. package/dist/contracts/v1/agent.js +8 -0
  94. package/dist/contracts/v1/agent.js.map +1 -0
  95. package/dist/contracts/v1/agentProfileManifest.d.ts +60 -0
  96. package/dist/contracts/v1/agentProfileManifest.d.ts.map +1 -0
  97. package/dist/contracts/v1/agentProfileManifest.js +9 -0
  98. package/dist/contracts/v1/agentProfileManifest.js.map +1 -0
  99. package/dist/contracts/v1/agentRules.d.ts +60 -0
  100. package/dist/contracts/v1/agentRules.d.ts.map +1 -0
  101. package/dist/contracts/v1/agentRules.js +10 -0
  102. package/dist/contracts/v1/agentRules.js.map +1 -0
  103. package/dist/contracts/v1/provider.d.ts +149 -0
  104. package/dist/contracts/v1/provider.d.ts.map +1 -0
  105. package/dist/contracts/v1/provider.js +7 -0
  106. package/dist/contracts/v1/provider.js.map +1 -0
  107. package/dist/contracts/v1/tool.d.ts +136 -0
  108. package/dist/contracts/v1/tool.d.ts.map +1 -0
  109. package/dist/contracts/v1/tool.js +7 -0
  110. package/dist/contracts/v1/tool.js.map +1 -0
  111. package/dist/contracts/v1/toolAccess.d.ts +43 -0
  112. package/dist/contracts/v1/toolAccess.d.ts.map +1 -0
  113. package/dist/contracts/v1/toolAccess.js +9 -0
  114. package/dist/contracts/v1/toolAccess.js.map +1 -0
  115. package/dist/core/adversarial.d.ts +38 -0
  116. package/dist/core/adversarial.d.ts.map +1 -0
  117. package/dist/core/adversarial.js +106 -0
  118. package/dist/core/adversarial.js.map +1 -0
  119. package/dist/core/adversarialCorrection.d.ts +22 -0
  120. package/dist/core/adversarialCorrection.d.ts.map +1 -0
  121. package/dist/core/adversarialCorrection.js +25 -0
  122. package/dist/core/adversarialCorrection.js.map +1 -0
  123. package/dist/core/agent.d.ts +331 -0
  124. package/dist/core/agent.d.ts.map +1 -0
  125. package/dist/core/agent.js +1637 -0
  126. package/dist/core/agent.js.map +1 -0
  127. package/dist/core/agentProfileManifest.d.ts +3 -0
  128. package/dist/core/agentProfileManifest.d.ts.map +1 -0
  129. package/dist/core/agentProfileManifest.js +188 -0
  130. package/dist/core/agentProfileManifest.js.map +1 -0
  131. package/dist/core/agentProfiles.d.ts +22 -0
  132. package/dist/core/agentProfiles.d.ts.map +1 -0
  133. package/dist/core/agentProfiles.js +35 -0
  134. package/dist/core/agentProfiles.js.map +1 -0
  135. package/dist/core/agentRegistry.d.ts +111 -0
  136. package/dist/core/agentRegistry.d.ts.map +1 -0
  137. package/dist/core/agentRegistry.js +229 -0
  138. package/dist/core/agentRegistry.js.map +1 -0
  139. package/dist/core/agentRulebook.d.ts +11 -0
  140. package/dist/core/agentRulebook.d.ts.map +1 -0
  141. package/dist/core/agentRulebook.js +136 -0
  142. package/dist/core/agentRulebook.js.map +1 -0
  143. package/dist/core/agentSchemaLoader.d.ts +131 -0
  144. package/dist/core/agentSchemaLoader.d.ts.map +1 -0
  145. package/dist/core/agentSchemaLoader.js +235 -0
  146. package/dist/core/agentSchemaLoader.js.map +1 -0
  147. package/dist/core/aiErrorFixer.d.ts +57 -0
  148. package/dist/core/aiErrorFixer.d.ts.map +1 -0
  149. package/dist/core/aiErrorFixer.js +214 -0
  150. package/dist/core/aiErrorFixer.js.map +1 -0
  151. package/dist/core/bashCommandGuidance.d.ts +16 -0
  152. package/dist/core/bashCommandGuidance.d.ts.map +1 -0
  153. package/dist/core/bashCommandGuidance.js +40 -0
  154. package/dist/core/bashCommandGuidance.js.map +1 -0
  155. package/dist/core/compactionNote.d.ts +13 -0
  156. package/dist/core/compactionNote.d.ts.map +1 -0
  157. package/dist/core/compactionNote.js +13 -0
  158. package/dist/core/compactionNote.js.map +1 -0
  159. package/dist/core/constants.d.ts +31 -0
  160. package/dist/core/constants.d.ts.map +1 -0
  161. package/dist/core/constants.js +62 -0
  162. package/dist/core/constants.js.map +1 -0
  163. package/dist/core/contextManager.d.ts +271 -0
  164. package/dist/core/contextManager.d.ts.map +1 -0
  165. package/dist/core/contextManager.js +1076 -0
  166. package/dist/core/contextManager.js.map +1 -0
  167. package/dist/core/contextUsage.d.ts +28 -0
  168. package/dist/core/contextUsage.d.ts.map +1 -0
  169. package/dist/core/contextUsage.js +62 -0
  170. package/dist/core/contextUsage.js.map +1 -0
  171. package/dist/core/contextWindow.d.ts +42 -0
  172. package/dist/core/contextWindow.d.ts.map +1 -0
  173. package/dist/core/contextWindow.js +127 -0
  174. package/dist/core/contextWindow.js.map +1 -0
  175. package/dist/core/customCommands.d.ts +19 -0
  176. package/dist/core/customCommands.d.ts.map +1 -0
  177. package/dist/core/customCommands.js +85 -0
  178. package/dist/core/customCommands.js.map +1 -0
  179. package/dist/core/diffPanel.d.ts +30 -0
  180. package/dist/core/diffPanel.d.ts.map +1 -0
  181. package/dist/core/diffPanel.js +48 -0
  182. package/dist/core/diffPanel.js.map +1 -0
  183. package/dist/core/errorClassification.d.ts +44 -0
  184. package/dist/core/errorClassification.d.ts.map +1 -0
  185. package/dist/core/errorClassification.js +333 -0
  186. package/dist/core/errorClassification.js.map +1 -0
  187. package/dist/core/errors/apiKeyErrors.d.ts +11 -0
  188. package/dist/core/errors/apiKeyErrors.d.ts.map +1 -0
  189. package/dist/core/errors/apiKeyErrors.js +159 -0
  190. package/dist/core/errors/apiKeyErrors.js.map +1 -0
  191. package/dist/core/errors/errorTypes.d.ts +111 -0
  192. package/dist/core/errors/errorTypes.d.ts.map +1 -0
  193. package/dist/core/errors/errorTypes.js +345 -0
  194. package/dist/core/errors/errorTypes.js.map +1 -0
  195. package/dist/core/errors/index.d.ts +50 -0
  196. package/dist/core/errors/index.d.ts.map +1 -0
  197. package/dist/core/errors/index.js +156 -0
  198. package/dist/core/errors/index.js.map +1 -0
  199. package/dist/core/errors/networkErrors.d.ts +14 -0
  200. package/dist/core/errors/networkErrors.d.ts.map +1 -0
  201. package/dist/core/errors/networkErrors.js +53 -0
  202. package/dist/core/errors/networkErrors.js.map +1 -0
  203. package/dist/core/errors/safetyValidator.d.ts +109 -0
  204. package/dist/core/errors/safetyValidator.d.ts.map +1 -0
  205. package/dist/core/errors/safetyValidator.js +272 -0
  206. package/dist/core/errors/safetyValidator.js.map +1 -0
  207. package/dist/core/errors.d.ts +4 -0
  208. package/dist/core/errors.d.ts.map +1 -0
  209. package/dist/core/errors.js +33 -0
  210. package/dist/core/errors.js.map +1 -0
  211. package/dist/core/failureRegistry.d.ts +30 -0
  212. package/dist/core/failureRegistry.d.ts.map +1 -0
  213. package/dist/core/failureRegistry.js +74 -0
  214. package/dist/core/failureRegistry.js.map +1 -0
  215. package/dist/core/fileMentions.d.ts +40 -0
  216. package/dist/core/fileMentions.d.ts.map +1 -0
  217. package/dist/core/fileMentions.js +136 -0
  218. package/dist/core/fileMentions.js.map +1 -0
  219. package/dist/core/finalResponseFormatter.d.ts +10 -0
  220. package/dist/core/finalResponseFormatter.d.ts.map +1 -0
  221. package/dist/core/finalResponseFormatter.js +14 -0
  222. package/dist/core/finalResponseFormatter.js.map +1 -0
  223. package/dist/core/guardrails.d.ts +146 -0
  224. package/dist/core/guardrails.d.ts.map +1 -0
  225. package/dist/core/guardrails.js +361 -0
  226. package/dist/core/guardrails.js.map +1 -0
  227. package/dist/core/hitl.d.ts +119 -0
  228. package/dist/core/hitl.d.ts.map +1 -0
  229. package/dist/core/hitl.js +396 -0
  230. package/dist/core/hitl.js.map +1 -0
  231. package/dist/core/hooks.d.ts +95 -0
  232. package/dist/core/hooks.d.ts.map +1 -0
  233. package/dist/core/hooks.js +236 -0
  234. package/dist/core/hooks.js.map +1 -0
  235. package/dist/core/hostedAuth.d.ts +88 -0
  236. package/dist/core/hostedAuth.d.ts.map +1 -0
  237. package/dist/core/hostedAuth.js +219 -0
  238. package/dist/core/hostedAuth.js.map +1 -0
  239. package/dist/core/inputProtection.d.ts +122 -0
  240. package/dist/core/inputProtection.d.ts.map +1 -0
  241. package/dist/core/inputProtection.js +422 -0
  242. package/dist/core/inputProtection.js.map +1 -0
  243. package/dist/core/modelDiscovery.d.ts +102 -0
  244. package/dist/core/modelDiscovery.d.ts.map +1 -0
  245. package/dist/core/modelDiscovery.js +416 -0
  246. package/dist/core/modelDiscovery.js.map +1 -0
  247. package/dist/core/multilinePasteHandler.d.ts +35 -0
  248. package/dist/core/multilinePasteHandler.d.ts.map +1 -0
  249. package/dist/core/multilinePasteHandler.js +81 -0
  250. package/dist/core/multilinePasteHandler.js.map +1 -0
  251. package/dist/core/permissionMode.d.ts +40 -0
  252. package/dist/core/permissionMode.d.ts.map +1 -0
  253. package/dist/core/permissionMode.js +86 -0
  254. package/dist/core/permissionMode.js.map +1 -0
  255. package/dist/core/postWriteDiagnostics.d.ts +32 -0
  256. package/dist/core/postWriteDiagnostics.d.ts.map +1 -0
  257. package/dist/core/postWriteDiagnostics.js +127 -0
  258. package/dist/core/postWriteDiagnostics.js.map +1 -0
  259. package/dist/core/preferences.d.ts +66 -0
  260. package/dist/core/preferences.d.ts.map +1 -0
  261. package/dist/core/preferences.js +310 -0
  262. package/dist/core/preferences.js.map +1 -0
  263. package/dist/core/quota.d.ts +61 -0
  264. package/dist/core/quota.d.ts.map +1 -0
  265. package/dist/core/quota.js +104 -0
  266. package/dist/core/quota.js.map +1 -0
  267. package/dist/core/quotaErrors.d.ts +42 -0
  268. package/dist/core/quotaErrors.d.ts.map +1 -0
  269. package/dist/core/quotaErrors.js +86 -0
  270. package/dist/core/quotaErrors.js.map +1 -0
  271. package/dist/core/refusalDetection.d.ts +2 -0
  272. package/dist/core/refusalDetection.d.ts.map +1 -0
  273. package/dist/core/refusalDetection.js +51 -0
  274. package/dist/core/refusalDetection.js.map +1 -0
  275. package/dist/core/relativeTime.d.ts +8 -0
  276. package/dist/core/relativeTime.d.ts.map +1 -0
  277. package/dist/core/relativeTime.js +29 -0
  278. package/dist/core/relativeTime.js.map +1 -0
  279. package/dist/core/resultVerification.d.ts +48 -0
  280. package/dist/core/resultVerification.d.ts.map +1 -0
  281. package/dist/core/resultVerification.js +127 -0
  282. package/dist/core/resultVerification.js.map +1 -0
  283. package/dist/core/rewind.d.ts +14 -0
  284. package/dist/core/rewind.d.ts.map +1 -0
  285. package/dist/core/rewind.js +25 -0
  286. package/dist/core/rewind.js.map +1 -0
  287. package/dist/core/schemaValidator.d.ts +49 -0
  288. package/dist/core/schemaValidator.d.ts.map +1 -0
  289. package/dist/core/schemaValidator.js +234 -0
  290. package/dist/core/schemaValidator.js.map +1 -0
  291. package/dist/core/secretStore.d.ts +59 -0
  292. package/dist/core/secretStore.d.ts.map +1 -0
  293. package/dist/core/secretStore.js +278 -0
  294. package/dist/core/secretStore.js.map +1 -0
  295. package/dist/core/sessionStorage.d.ts +10 -0
  296. package/dist/core/sessionStorage.d.ts.map +1 -0
  297. package/dist/core/sessionStorage.js +46 -0
  298. package/dist/core/sessionStorage.js.map +1 -0
  299. package/dist/core/sessionStore.d.ts +35 -0
  300. package/dist/core/sessionStore.d.ts.map +1 -0
  301. package/dist/core/sessionStore.js +190 -0
  302. package/dist/core/sessionStore.js.map +1 -0
  303. package/dist/core/shutdown.d.ts +34 -0
  304. package/dist/core/shutdown.d.ts.map +1 -0
  305. package/dist/core/shutdown.js +186 -0
  306. package/dist/core/shutdown.js.map +1 -0
  307. package/dist/core/slashCommands.d.ts +38 -0
  308. package/dist/core/slashCommands.d.ts.map +1 -0
  309. package/dist/core/slashCommands.js +72 -0
  310. package/dist/core/slashCommands.js.map +1 -0
  311. package/dist/core/subAgentNote.d.ts +15 -0
  312. package/dist/core/subAgentNote.d.ts.map +1 -0
  313. package/dist/core/subAgentNote.js +16 -0
  314. package/dist/core/subAgentNote.js.map +1 -0
  315. package/dist/core/sudoPasswordManager.d.ts +52 -0
  316. package/dist/core/sudoPasswordManager.d.ts.map +1 -0
  317. package/dist/core/sudoPasswordManager.js +115 -0
  318. package/dist/core/sudoPasswordManager.js.map +1 -0
  319. package/dist/core/taskCompletionDetector.d.ts +117 -0
  320. package/dist/core/taskCompletionDetector.d.ts.map +1 -0
  321. package/dist/core/taskCompletionDetector.js +532 -0
  322. package/dist/core/taskCompletionDetector.js.map +1 -0
  323. package/dist/core/testFailureMonitor.d.ts +67 -0
  324. package/dist/core/testFailureMonitor.d.ts.map +1 -0
  325. package/dist/core/testFailureMonitor.js +262 -0
  326. package/dist/core/testFailureMonitor.js.map +1 -0
  327. package/dist/core/thinkingVerbs.d.ts +31 -0
  328. package/dist/core/thinkingVerbs.d.ts.map +1 -0
  329. package/dist/core/thinkingVerbs.js +58 -0
  330. package/dist/core/thinkingVerbs.js.map +1 -0
  331. package/dist/core/toolPreconditions.d.ts +34 -0
  332. package/dist/core/toolPreconditions.d.ts.map +1 -0
  333. package/dist/core/toolPreconditions.js +242 -0
  334. package/dist/core/toolPreconditions.js.map +1 -0
  335. package/dist/core/toolRuntime.d.ts +193 -0
  336. package/dist/core/toolRuntime.d.ts.map +1 -0
  337. package/dist/core/toolRuntime.js +526 -0
  338. package/dist/core/toolRuntime.js.map +1 -0
  339. package/dist/core/turnGovernor.d.ts +63 -0
  340. package/dist/core/turnGovernor.d.ts.map +1 -0
  341. package/dist/core/turnGovernor.js +94 -0
  342. package/dist/core/turnGovernor.js.map +1 -0
  343. package/dist/core/types/utilityTypes.d.ts +183 -0
  344. package/dist/core/types/utilityTypes.d.ts.map +1 -0
  345. package/dist/core/types/utilityTypes.js +273 -0
  346. package/dist/core/types/utilityTypes.js.map +1 -0
  347. package/dist/core/types.d.ts +334 -0
  348. package/dist/core/types.d.ts.map +1 -0
  349. package/dist/core/types.js +76 -0
  350. package/dist/core/types.js.map +1 -0
  351. package/dist/core/updateChecker.d.ts +148 -0
  352. package/dist/core/updateChecker.d.ts.map +1 -0
  353. package/dist/core/updateChecker.js +605 -0
  354. package/dist/core/updateChecker.js.map +1 -0
  355. package/dist/core/usage.d.ts +28 -0
  356. package/dist/core/usage.d.ts.map +1 -0
  357. package/dist/core/usage.js +77 -0
  358. package/dist/core/usage.js.map +1 -0
  359. package/dist/headless/interactiveShell.d.ts +47 -0
  360. package/dist/headless/interactiveShell.d.ts.map +1 -0
  361. package/dist/headless/interactiveShell.js +2495 -0
  362. package/dist/headless/interactiveShell.js.map +1 -0
  363. package/dist/leanAgent.d.ts +73 -0
  364. package/dist/leanAgent.d.ts.map +1 -0
  365. package/dist/leanAgent.js +177 -0
  366. package/dist/leanAgent.js.map +1 -0
  367. package/dist/plugins/providers/deepseek/index.d.ts +12 -0
  368. package/dist/plugins/providers/deepseek/index.d.ts.map +1 -0
  369. package/dist/plugins/providers/deepseek/index.js +123 -0
  370. package/dist/plugins/providers/deepseek/index.js.map +1 -0
  371. package/dist/plugins/providers/index.d.ts +2 -0
  372. package/dist/plugins/providers/index.d.ts.map +1 -0
  373. package/dist/plugins/providers/index.js +10 -0
  374. package/dist/plugins/providers/index.js.map +1 -0
  375. package/dist/providers/baseProvider.d.ts +140 -0
  376. package/dist/providers/baseProvider.d.ts.map +1 -0
  377. package/dist/providers/baseProvider.js +230 -0
  378. package/dist/providers/baseProvider.js.map +1 -0
  379. package/dist/providers/openaiChatCompletionsProvider.d.ts +70 -0
  380. package/dist/providers/openaiChatCompletionsProvider.d.ts.map +1 -0
  381. package/dist/providers/openaiChatCompletionsProvider.js +971 -0
  382. package/dist/providers/openaiChatCompletionsProvider.js.map +1 -0
  383. package/dist/providers/providerFactory.d.ts +22 -0
  384. package/dist/providers/providerFactory.d.ts.map +1 -0
  385. package/dist/providers/providerFactory.js +25 -0
  386. package/dist/providers/providerFactory.js.map +1 -0
  387. package/dist/providers/resilientProvider.d.ts +96 -0
  388. package/dist/providers/resilientProvider.d.ts.map +1 -0
  389. package/dist/providers/resilientProvider.js +251 -0
  390. package/dist/providers/resilientProvider.js.map +1 -0
  391. package/dist/runtime/agentController.d.ts +137 -0
  392. package/dist/runtime/agentController.d.ts.map +1 -0
  393. package/dist/runtime/agentController.js +784 -0
  394. package/dist/runtime/agentController.js.map +1 -0
  395. package/dist/runtime/agentHost.d.ts +61 -0
  396. package/dist/runtime/agentHost.d.ts.map +1 -0
  397. package/dist/runtime/agentHost.js +158 -0
  398. package/dist/runtime/agentHost.js.map +1 -0
  399. package/dist/runtime/agentSession.d.ts +49 -0
  400. package/dist/runtime/agentSession.d.ts.map +1 -0
  401. package/dist/runtime/agentSession.js +218 -0
  402. package/dist/runtime/agentSession.js.map +1 -0
  403. package/dist/runtime/agentSpawningWiring.d.ts +32 -0
  404. package/dist/runtime/agentSpawningWiring.d.ts.map +1 -0
  405. package/dist/runtime/agentSpawningWiring.js +114 -0
  406. package/dist/runtime/agentSpawningWiring.js.map +1 -0
  407. package/dist/runtime/node.d.ts +7 -0
  408. package/dist/runtime/node.d.ts.map +1 -0
  409. package/dist/runtime/node.js +50 -0
  410. package/dist/runtime/node.js.map +1 -0
  411. package/dist/runtime/universal.d.ts +18 -0
  412. package/dist/runtime/universal.d.ts.map +1 -0
  413. package/dist/runtime/universal.js +21 -0
  414. package/dist/runtime/universal.js.map +1 -0
  415. package/dist/shell/liveStatus.d.ts +27 -0
  416. package/dist/shell/liveStatus.d.ts.map +1 -0
  417. package/dist/shell/liveStatus.js +53 -0
  418. package/dist/shell/liveStatus.js.map +1 -0
  419. package/dist/shell/systemPrompt.d.ts +12 -0
  420. package/dist/shell/systemPrompt.d.ts.map +1 -0
  421. package/dist/shell/systemPrompt.js +16 -0
  422. package/dist/shell/systemPrompt.js.map +1 -0
  423. package/dist/shell/toolPresentation.d.ts +54 -0
  424. package/dist/shell/toolPresentation.d.ts.map +1 -0
  425. package/dist/shell/toolPresentation.js +334 -0
  426. package/dist/shell/toolPresentation.js.map +1 -0
  427. package/dist/tools/bashTools.d.ts +11 -0
  428. package/dist/tools/bashTools.d.ts.map +1 -0
  429. package/dist/tools/bashTools.js +785 -0
  430. package/dist/tools/bashTools.js.map +1 -0
  431. package/dist/tools/diffUtils.d.ts +43 -0
  432. package/dist/tools/diffUtils.d.ts.map +1 -0
  433. package/dist/tools/diffUtils.js +607 -0
  434. package/dist/tools/diffUtils.js.map +1 -0
  435. package/dist/tools/editTools.d.ts +29 -0
  436. package/dist/tools/editTools.d.ts.map +1 -0
  437. package/dist/tools/editTools.js +792 -0
  438. package/dist/tools/editTools.js.map +1 -0
  439. package/dist/tools/fileChangeTracker.d.ts +47 -0
  440. package/dist/tools/fileChangeTracker.d.ts.map +1 -0
  441. package/dist/tools/fileChangeTracker.js +154 -0
  442. package/dist/tools/fileChangeTracker.js.map +1 -0
  443. package/dist/tools/fileReadTracker.d.ts +69 -0
  444. package/dist/tools/fileReadTracker.d.ts.map +1 -0
  445. package/dist/tools/fileReadTracker.js +213 -0
  446. package/dist/tools/fileReadTracker.js.map +1 -0
  447. package/dist/tools/fileTools.d.ts +3 -0
  448. package/dist/tools/fileTools.d.ts.map +1 -0
  449. package/dist/tools/fileTools.js +389 -0
  450. package/dist/tools/fileTools.js.map +1 -0
  451. package/dist/tools/grepTools.d.ts +3 -0
  452. package/dist/tools/grepTools.d.ts.map +1 -0
  453. package/dist/tools/grepTools.js +137 -0
  454. package/dist/tools/grepTools.js.map +1 -0
  455. package/dist/tools/hitlTools.d.ts +7 -0
  456. package/dist/tools/hitlTools.d.ts.map +1 -0
  457. package/dist/tools/hitlTools.js +185 -0
  458. package/dist/tools/hitlTools.js.map +1 -0
  459. package/dist/tools/memoryTools.d.ts +27 -0
  460. package/dist/tools/memoryTools.d.ts.map +1 -0
  461. package/dist/tools/memoryTools.js +197 -0
  462. package/dist/tools/memoryTools.js.map +1 -0
  463. package/dist/tools/notebookTools.d.ts +20 -0
  464. package/dist/tools/notebookTools.d.ts.map +1 -0
  465. package/dist/tools/notebookTools.js +140 -0
  466. package/dist/tools/notebookTools.js.map +1 -0
  467. package/dist/tools/searchTools.d.ts +12 -0
  468. package/dist/tools/searchTools.d.ts.map +1 -0
  469. package/dist/tools/searchTools.js +414 -0
  470. package/dist/tools/searchTools.js.map +1 -0
  471. package/dist/tools/skillTools.d.ts +24 -0
  472. package/dist/tools/skillTools.d.ts.map +1 -0
  473. package/dist/tools/skillTools.js +140 -0
  474. package/dist/tools/skillTools.js.map +1 -0
  475. package/dist/tools/todoTools.d.ts +23 -0
  476. package/dist/tools/todoTools.d.ts.map +1 -0
  477. package/dist/tools/todoTools.js +120 -0
  478. package/dist/tools/todoTools.js.map +1 -0
  479. package/dist/tools/webTools.d.ts +26 -0
  480. package/dist/tools/webTools.d.ts.map +1 -0
  481. package/dist/tools/webTools.js +467 -0
  482. package/dist/tools/webTools.js.map +1 -0
  483. package/dist/ui/ink/App.d.ts +53 -0
  484. package/dist/ui/ink/App.d.ts.map +1 -0
  485. package/dist/ui/ink/App.js +13 -0
  486. package/dist/ui/ink/App.js.map +1 -0
  487. package/dist/ui/ink/ChatStatic.d.ts +30 -0
  488. package/dist/ui/ink/ChatStatic.d.ts.map +1 -0
  489. package/dist/ui/ink/ChatStatic.js +83 -0
  490. package/dist/ui/ink/ChatStatic.js.map +1 -0
  491. package/dist/ui/ink/InkPromptController.d.ts +321 -0
  492. package/dist/ui/ink/InkPromptController.d.ts.map +1 -0
  493. package/dist/ui/ink/InkPromptController.js +667 -0
  494. package/dist/ui/ink/InkPromptController.js.map +1 -0
  495. package/dist/ui/ink/Menu.d.ts +21 -0
  496. package/dist/ui/ink/Menu.d.ts.map +1 -0
  497. package/dist/ui/ink/Menu.js +61 -0
  498. package/dist/ui/ink/Menu.js.map +1 -0
  499. package/dist/ui/ink/Prompt.d.ts +47 -0
  500. package/dist/ui/ink/Prompt.d.ts.map +1 -0
  501. package/dist/ui/ink/Prompt.js +571 -0
  502. package/dist/ui/ink/Prompt.js.map +1 -0
  503. package/dist/ui/ink/StatusLine.d.ts +35 -0
  504. package/dist/ui/ink/StatusLine.d.ts.map +1 -0
  505. package/dist/ui/ink/StatusLine.js +66 -0
  506. package/dist/ui/ink/StatusLine.js.map +1 -0
  507. package/dist/ui/ink/pasteBuffer.d.ts +44 -0
  508. package/dist/ui/ink/pasteBuffer.d.ts.map +1 -0
  509. package/dist/ui/ink/pasteBuffer.js +73 -0
  510. package/dist/ui/ink/pasteBuffer.js.map +1 -0
  511. package/dist/ui/theme.d.ts +351 -0
  512. package/dist/ui/theme.d.ts.map +1 -0
  513. package/dist/ui/theme.js +435 -0
  514. package/dist/ui/theme.js.map +1 -0
  515. package/dist/utils/analytics.d.ts +2 -0
  516. package/dist/utils/analytics.d.ts.map +1 -0
  517. package/dist/utils/analytics.js +51 -0
  518. package/dist/utils/analytics.js.map +1 -0
  519. package/dist/utils/asyncUtils.d.ts +95 -0
  520. package/dist/utils/asyncUtils.d.ts.map +1 -0
  521. package/dist/utils/asyncUtils.js +286 -0
  522. package/dist/utils/asyncUtils.js.map +1 -0
  523. package/dist/utils/debugLogger.d.ts +6 -0
  524. package/dist/utils/debugLogger.d.ts.map +1 -0
  525. package/dist/utils/debugLogger.js +39 -0
  526. package/dist/utils/debugLogger.js.map +1 -0
  527. package/dist/utils/errorUtils.d.ts +12 -0
  528. package/dist/utils/errorUtils.d.ts.map +1 -0
  529. package/dist/utils/errorUtils.js +83 -0
  530. package/dist/utils/errorUtils.js.map +1 -0
  531. package/dist/utils/frontmatter.d.ts +10 -0
  532. package/dist/utils/frontmatter.d.ts.map +1 -0
  533. package/dist/utils/frontmatter.js +78 -0
  534. package/dist/utils/frontmatter.js.map +1 -0
  535. package/dist/utils/packageInfo.d.ts +14 -0
  536. package/dist/utils/packageInfo.d.ts.map +1 -0
  537. package/dist/utils/packageInfo.js +45 -0
  538. package/dist/utils/packageInfo.js.map +1 -0
  539. package/dist/utils/planFormatter.d.ts +34 -0
  540. package/dist/utils/planFormatter.d.ts.map +1 -0
  541. package/dist/utils/planFormatter.js +141 -0
  542. package/dist/utils/planFormatter.js.map +1 -0
  543. package/dist/utils/securityUtils.d.ts +132 -0
  544. package/dist/utils/securityUtils.d.ts.map +1 -0
  545. package/dist/utils/securityUtils.js +324 -0
  546. package/dist/utils/securityUtils.js.map +1 -0
  547. package/dist/utils/statusReporter.d.ts +6 -0
  548. package/dist/utils/statusReporter.d.ts.map +1 -0
  549. package/dist/utils/statusReporter.js +26 -0
  550. package/dist/utils/statusReporter.js.map +1 -0
  551. package/dist/workspace.d.ts +8 -0
  552. package/dist/workspace.d.ts.map +1 -0
  553. package/dist/workspace.js +135 -0
  554. package/dist/workspace.js.map +1 -0
  555. package/dist/workspace.validator.d.ts +49 -0
  556. package/dist/workspace.validator.d.ts.map +1 -0
  557. package/dist/workspace.validator.js +215 -0
  558. package/dist/workspace.validator.js.map +1 -0
  559. package/package.json +116 -0
  560. package/scripts/postinstall.cjs +56 -0
@@ -0,0 +1,2495 @@
1
+ /**
2
+ * Interactive Shell - Full interactive CLI experience with rich UI.
3
+ *
4
+ * Usage:
5
+ * agi # Start interactive shell
6
+ * agi "initial prompt" # Start with initial prompt
7
+ *
8
+ * Features:
9
+ * - Rich terminal UI with status bar
10
+ * - Command history
11
+ * - Streaming responses
12
+ * - Tool execution display
13
+ * - Ctrl+C to interrupt
14
+ */
15
+ import { stdin, stdout, exit } from 'node:process';
16
+ import { readFileSync } from 'node:fs';
17
+ import { resolve, dirname, relative } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { exec as childExec } from 'node:child_process';
20
+ import { promisify } from 'node:util';
21
+ import chalk from 'chalk';
22
+ import { getHITL, hitlEvents } from '../core/hitl.js';
23
+ // Connector imports removed — CLI is local-only, no GitHub gate.
24
+ // Stub functions (antiTermination removed)
25
+ const initializeProtection = (_config) => { };
26
+ const enterCriticalSection = (_name) => { };
27
+ const exitCriticalSection = (_name) => { };
28
+ // Import real shutdown handler for reliable Ctrl+C handling
29
+ import { authorizedShutdown, installSignalHandlers, onShutdown } from '../core/shutdown.js';
30
+ import { resolveProfileConfig } from '../config.js';
31
+ import { createAgentController } from '../runtime/agentController.js';
32
+ import { expandFileMentions, listWorkspaceFiles } from '../core/fileMentions.js';
33
+ import { resolveWorkspaceCaptureOptions, buildWorkspaceContext } from '../workspace.js';
34
+ import { loadAllSecrets, listSecretDefinitions, setSecretValue, getSecretValue, getSecretDefinition, classifyKeyEntry } from '../core/secretStore.js';
35
+ import { resolveKeyMode, keyModeLine, setPreferOwnKeys, clearHostedSession, loginViaLoopback } from '../core/hostedAuth.js';
36
+ import { appendMemoryNote } from '../tools/memoryTools.js';
37
+ import { recordDeepSeekUsage, getUsage, TAVILY_MONTHLY_FREE, TAVILY_ONE_TIME_BONUS } from '../core/usage.js';
38
+ import { listSessions, loadSessionById, saveSessionSnapshot } from '../core/sessionStore.js';
39
+ import { relativeTime } from '../core/relativeTime.js';
40
+ import { getModelContextInfo } from '../core/contextWindow.js';
41
+ import { computeContextUsage, formatTokenCount } from '../core/contextUsage.js';
42
+ import { getChangedFiles, revertAllChanges, hasChangesToRevert } from '../tools/fileChangeTracker.js';
43
+ import { renderChangePanel } from '../core/diffPanel.js';
44
+ import { rewindPreviewLines, rewindResultLine } from '../core/rewind.js';
45
+ import { formatCompactionNote } from '../core/compactionNote.js';
46
+ import { formatSubAgentStart, formatSubAgentComplete } from '../core/subAgentNote.js';
47
+ import { getConfiguredProviders, getProvidersStatus, quickCheckProviders, getCachedDiscoveredModels, sortModelsByPriority } from '../core/modelDiscovery.js';
48
+ import { saveModelPreference } from '../core/preferences.js';
49
+ import { setDebugMode, debugSnippet } from '../utils/debugLogger.js';
50
+ const exec = promisify(childExec);
51
+ import { ensureNextSteps } from '../core/finalResponseFormatter.js';
52
+ import { getTaskCompletionDetector, detectFailingTestOrBuild } from '../core/taskCompletionDetector.js';
53
+ import { TurnGovernor, pendingTodos, nextTodoPrompt } from '../core/turnGovernor.js';
54
+ import { FailureRegistry } from '../core/failureRegistry.js';
55
+ import { buildAdversarialCorrectionPrompt, MAX_ADVERSARIAL_CORRECTIONS } from '../core/adversarialCorrection.js';
56
+ import { getCurrentTodos } from '../tools/todoTools.js';
57
+ import { checkForUpdates, performBackgroundUpdate } from '../core/updateChecker.js';
58
+ import { startNewRun } from '../tools/fileChangeTracker.js';
59
+ import { onSudoPasswordNeeded, offSudoPasswordNeeded, provideSudoPassword } from '../core/sudoPasswordManager.js';
60
+ import { reportStatus, setStatusSink } from '../utils/statusReporter.js';
61
+ import { isSafetyRefusal } from '../core/refusalDetection.js';
62
+ import { formatToolCall, toolActivityLabel, formatToolResult, formatToolError } from '../shell/toolPresentation.js';
63
+ // Tool-result display (ANSI stripping, summarisation, the `⎿` block) now lives
64
+ // in ../shell/toolPresentation.ts — the shell just emits the formatted strings.
65
+ // Timeout constants for regular prompt processing (reasoning models like DeepSeek)
66
+ const PROMPT_REASONING_TIMEOUT_MS = 60 * 1000; // 60 seconds max for reasoning-only without action
67
+ // Per-step timeout: how long we'll wait for the *next* event before
68
+ // declaring the stream stuck and bailing out. Set generously (10 min) so
69
+ // long-running tool calls (a build, a slow `npm install`, etc.) don't
70
+ // trip it, but short enough that a dead provider / network drop doesn't
71
+ // leave the user staring at a forever-spinner with Ctrl+C as their only
72
+ // escape. iterateWithTimeout resets this per-event, so it only fires on
73
+ // genuine inactivity. Override with TRENCHWORK_STEP_TIMEOUT_MS for tests.
74
+ const PROMPT_STEP_TIMEOUT_MS = (() => {
75
+ const env = process.env['TRENCHWORK_STEP_TIMEOUT_MS'];
76
+ const parsed = env ? Number(env) : NaN;
77
+ if (Number.isFinite(parsed) && parsed > 0)
78
+ return parsed;
79
+ return 10 * 60 * 1000;
80
+ })();
81
+ const HITL_TOOL_PREFIX = 'HITL_';
82
+ const isHitlToolName = (toolName) => toolName.startsWith(HITL_TOOL_PREFIX);
83
+ /**
84
+ * Iterate over an async iterator with a timeout per iteration.
85
+ * If no event is received within the timeout, yields a special timeout marker.
86
+ * Emits timeout markers without aborting the underlying iterator.
87
+ * Pass Infinity to disable timeouts entirely.
88
+ */
89
+ async function* iterateWithTimeout(iterator, timeoutMs, onTimeout) {
90
+ const asyncIterator = iterator[Symbol.asyncIterator]();
91
+ let pending = null;
92
+ let done = false;
93
+ // If timeout is Infinity or not a positive finite number, disable timeout entirely
94
+ const timeoutDisabled = !Number.isFinite(timeoutMs) || timeoutMs <= 0;
95
+ try {
96
+ while (true) {
97
+ if (!pending) {
98
+ pending = asyncIterator.next();
99
+ }
100
+ let result;
101
+ if (timeoutDisabled) {
102
+ // No timeout - just wait for the next value
103
+ result = await pending;
104
+ }
105
+ else {
106
+ // Race between pending result and timeout
107
+ const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve({ __timeout: true }), timeoutMs));
108
+ result = await Promise.race([pending, timeoutPromise]);
109
+ }
110
+ if ('__timeout' in result) {
111
+ onTimeout?.();
112
+ yield result;
113
+ continue;
114
+ }
115
+ pending = null;
116
+ if (result.done) {
117
+ done = true;
118
+ return;
119
+ }
120
+ yield result.value;
121
+ }
122
+ }
123
+ finally {
124
+ if (!done && typeof asyncIterator.return === 'function') {
125
+ try {
126
+ await asyncIterator.return(undefined);
127
+ }
128
+ catch {
129
+ // Ignore return errors
130
+ }
131
+ }
132
+ }
133
+ }
134
+ let cachedVersion = null;
135
+ // Get version from package.json
136
+ function getVersion() {
137
+ if (cachedVersion)
138
+ return cachedVersion;
139
+ try {
140
+ const __filename = fileURLToPath(import.meta.url);
141
+ const pkgPath = resolve(dirname(__filename), '../../package.json');
142
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
143
+ cachedVersion = pkg.version || '0.0.0';
144
+ return cachedVersion;
145
+ }
146
+ catch {
147
+ return '0.0.0';
148
+ }
149
+ }
150
+ /** Inner content of the welcome box (plain, no border/colour). */
151
+ function welcomeBodyLines(input) {
152
+ const title = input.version ? `✻ Welcome to Trenchwork Coder ${input.version}` : '✻ Welcome to Trenchwork Coder';
153
+ const body = [title, ''];
154
+ const mode = input.keyMode ?? (input.hasApiKey ? 'own' : 'none');
155
+ if (mode === 'hosted') {
156
+ // Signed in — running on hosted keys. The mode line names the account so
157
+ // it's unmistakable this is NOT the user's own key.
158
+ body.push(input.keyModeLine ?? 'Signed in · using hosted keys');
159
+ }
160
+ else if (mode === 'own') {
161
+ body.push(`${input.model} · ${input.provider}`, `Key: ${input.maskedKey} · /help for commands`);
162
+ }
163
+ else {
164
+ body.push('⚠ No DeepSeek API key configured', '', '/login Sign in with Google for hosted keys', '', 'Or bring your own:', ' /key sk-… DeepSeek (required) · platform.deepseek.com', ' /key tvly-… Tavily web search (optional) · tavily.com');
165
+ }
166
+ if (input.cwd)
167
+ body.push(`cwd: ${input.cwd}`);
168
+ return body;
169
+ }
170
+ /**
171
+ * Wrap content lines in a Claude-Code-style rounded box (╭╮╰╯). `paint`
172
+ * colours an already-padded content cell; `border` colours the frame. Both
173
+ * default to identity so the pure version stays ANSI-free.
174
+ */
175
+ function roundedBox(content, paint = (s) => s, border = (s) => s) {
176
+ const width = Math.min(content.reduce((m, c) => Math.max(m, c.length), 0), 72);
177
+ const pad = (c) => c + ' '.repeat(Math.max(0, width - c.length));
178
+ const rule = '─'.repeat(width + 2);
179
+ return [
180
+ border(`╭${rule}╮`),
181
+ ...content.map((c) => `${border('│')} ${paint(pad(c))} ${border('│')}`),
182
+ border(`╰${rule}╯`),
183
+ ];
184
+ }
185
+ /**
186
+ * Compose the lines shown when the interactive shell opens. Deliberately NOT a
187
+ * marketing splash — bare `trenchwork` opens straight into the chat (like
188
+ * `claude`); this is the load-bearing welcome: a sparkle, the name, and either
189
+ * how to set a key or the active model + masked key, inside a rounded box that
190
+ * mirrors Claude Code's. Pure (no chalk/ANSI, no I/O) so the "no marketing
191
+ * splash, key guidance kept" contract is unit-testable without a PTY. The live
192
+ * renderer colourises equivalent content; this is the source of truth for
193
+ * WHICH lines appear.
194
+ */
195
+ export function composeWelcomeLines(input) {
196
+ return ['', ...(input.updateLines ?? []), ...roundedBox(welcomeBodyLines(input)), ''];
197
+ }
198
+ /**
199
+ * Run the fully interactive shell with rich UI.
200
+ */
201
+ export async function runInteractiveShell(options) {
202
+ // Install signal handlers FIRST for reliable Ctrl+C handling
203
+ installSignalHandlers();
204
+ // Initialize protection systems
205
+ initializeProtection({
206
+ interceptSignals: true,
207
+ monitorResources: true,
208
+ armorExceptions: true,
209
+ enableWatchdog: true,
210
+ verbose: process.env['TRENCHWORK_DEBUG'] === '1',
211
+ });
212
+ // The CLI is interactive-only. There is no piped / one-shot / headless
213
+ // mode — every session runs through the Ink renderer against a live
214
+ // terminal. If stdin or stdout isn't a TTY, fail fast with a clear
215
+ // message rather than emitting unrenderable escape sequences into a
216
+ // pipe.
217
+ if (!stdin.isTTY || !stdout.isTTY) {
218
+ reportStatus('trenchwork requires an interactive terminal. Run it directly in a TTY (no pipes, no shell redirection).');
219
+ exit(1);
220
+ }
221
+ loadAllSecrets();
222
+ // argv intentionally unused — the bin is shell-only. Any tokens after
223
+ // `trenchwork` are ignored on purpose; configuration lives in /secrets,
224
+ // /model, /auto, etc. The options.argv field stays only because tests
225
+ // pass it; it does not affect runtime.
226
+ void options;
227
+ const profile = resolveProfile();
228
+ const workingDir = process.cwd();
229
+ const workspaceOptions = resolveWorkspaceCaptureOptions(process.env);
230
+ const workspaceContext = buildWorkspaceContext(workingDir, workspaceOptions);
231
+ // Resolve profile config for model info
232
+ const profileConfig = resolveProfileConfig(profile, workspaceContext);
233
+ // Create agent controller
234
+ const controller = await createAgentController({
235
+ profile,
236
+ workingDir,
237
+ workspaceContext,
238
+ env: process.env,
239
+ });
240
+ // Create the interactive shell instance
241
+ const shell = new InteractiveShell(controller, profile, profileConfig, workingDir);
242
+ await shell.run();
243
+ }
244
+ class InteractiveShell {
245
+ controller;
246
+ profile;
247
+ profileConfig;
248
+ workingDir;
249
+ // The shell holds an `IPromptController`-shaped value. The CLI has a
250
+ // single renderer — Ink, via InkPromptController. `any` here keeps
251
+ // existing call signatures unchanged; the interface declares the
252
+ // same surface but TS would otherwise insist we touch every call
253
+ // site to declare nullability.
254
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
255
+ promptController = null;
256
+ isProcessing = false;
257
+ // Full result + tool of the last tool-result that was TRUNCATED for display,
258
+ // so Ctrl+O can expand it (null when the last result fit, or none yet).
259
+ lastExpandableResult = null;
260
+ // Newer npm version detected at startup (null if up to date / unchecked).
261
+ pendingUpdate = null;
262
+ shouldExit = false;
263
+ pendingPrompts = [];
264
+ debugEnabled = false;
265
+ // Stable id for THIS run's persisted session, so each autosave updates the
266
+ // same snapshot in place. Assigned from the first saveSessionSnapshot; set
267
+ // to a resumed session's id after /resume so the restored thread continues.
268
+ sessionId = null;
269
+ // Real input-token count from the provider's last response (= tokens
270
+ // currently occupying the context window). Drives the accurate "% context
271
+ // left" chrome indicator and the /context view.
272
+ lastInputTokens = null;
273
+ ctrlCCount = 0;
274
+ lastCtrlCTime = 0;
275
+ // Set when the user Ctrl+C interrupts a run; suppresses the auto-continue
276
+ // re-launch in the finally block of processPrompt so the agent doesn't
277
+ // immediately resume the work the user just cancelled. Cleared when the
278
+ // user submits a fresh prompt.
279
+ userInterruptedRun = false;
280
+ cachedProviders = null;
281
+ secretInputMode = {
282
+ active: false,
283
+ secretId: null,
284
+ queue: [],
285
+ };
286
+ pendingModelSwitch = null;
287
+ currentResponseBuffer = '';
288
+ // The turn's final assistant text, captured BEFORE currentResponseBuffer is
289
+ // cleared on message.complete. The auto-continue refusal/completion/governor
290
+ // reads run in the `finally`, AFTER that clear, so reading the buffer there saw
291
+ // '' and blinded them (completion detection + safety-refusal both need the
292
+ // text). This mirrors the buffer's content but is never cleared mid-turn.
293
+ finalResponseText = '';
294
+ // Store original prompt for auto-continuation
295
+ originalPromptForAutoContinue = null;
296
+ // (Pinned prompt removed per request — field intentionally absent.)
297
+ // Bounds + stall-detects the auto-continue loop per user request, and drives
298
+ // continuation from the live TODO plan (see src/core/turnGovernor.ts). Reset
299
+ // when a fresh user prompt arrives.
300
+ autoGovernor = new TurnGovernor();
301
+ // Remembers recurring error signatures across auto-continue turns so the
302
+ // agent stops re-trying the same dead end (see src/core/failureRegistry.ts).
303
+ failureRegistry = new FailureRegistry();
304
+ // Adversarial auto-correction: how many bounded re-fixes the reviewer has
305
+ // triggered for the CURRENT user request (capped). Reset on a fresh prompt;
306
+ // the findings themselves are a per-turn local in processPrompt.
307
+ adversarialCorrectionCount = 0;
308
+ constructor(controller, profile, profileConfig, workingDir) {
309
+ this.controller = controller;
310
+ this.profile = profile;
311
+ this.profileConfig = profileConfig;
312
+ this.workingDir = workingDir;
313
+ // Pre-fetch provider status in background
314
+ void this.fetchProviders();
315
+ }
316
+ async fetchProviders() {
317
+ try {
318
+ this.cachedProviders = await quickCheckProviders();
319
+ }
320
+ catch {
321
+ this.cachedProviders = [];
322
+ }
323
+ }
324
+ validateRequiredApiKeys() {
325
+ const missingKeys = [];
326
+ // Check DeepSeek API key (required)
327
+ if (!getSecretValue('DEEPSEEK_API_KEY')) {
328
+ missingKeys.push('DEEPSEEK_API_KEY');
329
+ }
330
+ // Prompt for missing keys directly without showing warning
331
+ if (missingKeys.length > 0 && this.promptController) {
332
+ // Queue all missing keys for input
333
+ this.secretInputMode.queue = missingKeys.slice(1); // Rest of the keys
334
+ const first = missingKeys[0];
335
+ if (first) {
336
+ // Set secret mode immediately to mask input
337
+ this.secretInputMode.active = true;
338
+ this.secretInputMode.secretId = first;
339
+ this.promptController.setSecretMode(true);
340
+ // Show the inline panel with instructions
341
+ const secrets = listSecretDefinitions();
342
+ const secret = secrets.find(s => s.id === first);
343
+ if (secret && this.promptController.supportsInlinePanel()) {
344
+ const lines = [
345
+ chalk.bold.hex('#ece6da')(`Set ${secret.label}`),
346
+ chalk.dim(secret.description),
347
+ '',
348
+ chalk.dim('Enter value (or press Enter to skip)'),
349
+ ];
350
+ this.promptController.setInlinePanel(lines);
351
+ this.promptController.setStatusMessage(`Enter ${secret.label}...`);
352
+ }
353
+ }
354
+ }
355
+ }
356
+ queuePrompt(prompt) {
357
+ this.pendingPrompts.push(prompt);
358
+ }
359
+ async run() {
360
+ // createPromptController returns the Ink-backed controller
361
+ // (src/ui/ink/InkPromptController.ts) — the only renderer. The
362
+ // dynamic import keeps React/Ink off the cold-start path until
363
+ // the interactive shell actually starts.
364
+ const { createPromptController } = await import('../ui/ink/InkPromptController.js');
365
+ this.promptController = await createPromptController(stdin, stdout, {
366
+ onSubmit: (text) => this.handleSubmit(text),
367
+ onQueue: (text) => this.queuePrompt(text),
368
+ onInterrupt: () => this.handleInterrupt(),
369
+ onExit: () => this.handleExit(),
370
+ onCtrlC: (info) => this.handleCtrlC(info),
371
+ onToggleAutoContinue: () => this.handleAutoContinueToggle(),
372
+ onToggleHITL: () => this.handleHITLToggle(),
373
+ onCyclePermissionMode: (mode) => this.handlePermissionModeChange(mode),
374
+ onExpandToolResult: () => this.handleExpandToolResult(),
375
+ // Esc interrupts a running turn (handleInterrupt no-ops when idle), so
376
+ // the spinner's "esc to interrupt" is real. Ctrl+C still works too.
377
+ onEscape: () => this.handleInterrupt(),
378
+ onShowShortcuts: () => this.showKeyboardShortcuts(),
379
+ onDismissPanel: () => this.dismissInlinePanel(),
380
+ });
381
+ // Register cleanup callback for graceful shutdown
382
+ onShutdown(() => {
383
+ this.shouldExit = true;
384
+ this.promptController?.stop();
385
+ setStatusSink(null);
386
+ });
387
+ setStatusSink((message) => this.promptController?.setStatusMessage(message));
388
+ // Hand the terminal off to the HITL prompt while it's open: suspend
389
+ // prompt rendering and detach our keypress handler so arrow keys aren't
390
+ // double-consumed. Restore both when the prompt closes so the next turn's
391
+ // input works correctly.
392
+ const onHitlOpen = () => {
393
+ const r = this.promptController?.getRenderer();
394
+ if (!r)
395
+ return;
396
+ try {
397
+ r.suspendPromptRendering();
398
+ }
399
+ catch { /* ignore */ }
400
+ try {
401
+ r.suspendInputCapture();
402
+ }
403
+ catch { /* ignore */ }
404
+ };
405
+ const onHitlClose = () => {
406
+ const r = this.promptController?.getRenderer();
407
+ if (!r)
408
+ return;
409
+ try {
410
+ r.resumeInputCapture();
411
+ }
412
+ catch { /* ignore */ }
413
+ try {
414
+ r.resumePromptRendering(true);
415
+ }
416
+ catch { /* ignore */ }
417
+ };
418
+ hitlEvents.on('prompt-open', onHitlOpen);
419
+ hitlEvents.on('prompt-close', onHitlClose);
420
+ onShutdown(() => {
421
+ hitlEvents.removeListener('prompt-open', onHitlOpen);
422
+ hitlEvents.removeListener('prompt-close', onHitlClose);
423
+ });
424
+ // Start the UI
425
+ this.promptController.start();
426
+ this.applyDebugState(this.debugEnabled);
427
+ // Build the @-mention completion file list (bounded walk; new files appear
428
+ // on the next launch). Best-effort — a scan failure must not block the UI.
429
+ try {
430
+ this.promptController.setCompletionFiles(listWorkspaceFiles(this.workingDir));
431
+ }
432
+ catch { /* ignore — completion is a convenience */ }
433
+ // Set up sudo password prompt handler
434
+ this.setupSudoPasswordHandler();
435
+ // Set initial status
436
+ this.promptController.setChromeMeta({
437
+ directory: this.workingDir,
438
+ });
439
+ // Show welcome message
440
+ await this.showWelcome();
441
+ // Pinned prompt loading removed — feature stripped per request.
442
+ // TEST SEAM (guarded; never active unless TRENCHWORK_TEST_FORCE_BUSY_MS + SKIP_AUTH):
443
+ // Lets PTY E2E harness drive the exact live follow-up queue paths
444
+ // (handleSubmit during isProcessing, transient queued UI, drain) with
445
+ // real binary + real keystrokes, no LLM key or network required.
446
+ const forceBusyMs = Number(process.env['TRENCHWORK_TEST_FORCE_BUSY_MS'] || '0');
447
+ if (forceBusyMs > 0) {
448
+ this.isProcessing = true;
449
+ this.promptController?.setStreaming(true);
450
+ this.promptController?.setActivityMessage('TEST BUSY (seam for queue E2E)');
451
+ setTimeout(() => {
452
+ this.isProcessing = false;
453
+ this.promptController?.setStreaming(false);
454
+ this.promptController?.setActivityMessage(null);
455
+ this.promptController?.forceRender();
456
+ // Explicitly drain here (exercises the real drain + processPrompt path
457
+ // even though the fake "run" had no controller.send). Pending items
458
+ // will hit the normal early guard + error path, but the queue/dequeue
459
+ // logic itself runs for the test assertions.
460
+ if (this.pendingPrompts.length > 0 && !this.shouldExit) {
461
+ const next = this.pendingPrompts.shift();
462
+ if (next) {
463
+ const r = this.promptController?.getRenderer();
464
+ r?.setFollowUpQueueMode(false);
465
+ r?.addUserHistoryItem(next);
466
+ r?.setQueuedPrompts(this.pendingPrompts.slice());
467
+ void this.processPrompt(next).catch(() => { });
468
+ }
469
+ }
470
+ }, forceBusyMs);
471
+ }
472
+ // Process any queued prompts
473
+ if (this.pendingPrompts.length > 0) {
474
+ const prompts = this.pendingPrompts.splice(0);
475
+ for (const prompt of prompts) {
476
+ await this.processPrompt(prompt);
477
+ }
478
+ }
479
+ // Keep running until exit
480
+ await this.waitForExit();
481
+ }
482
+ async showWelcome() {
483
+ const renderer = this.promptController?.getRenderer();
484
+ if (!renderer)
485
+ return;
486
+ const version = getVersion();
487
+ // Append to existing terminal history — do not clear scrollback.
488
+ // Check if DeepSeek API key is set
489
+ const apiKey = process.env.DEEPSEEK_API_KEY?.trim() || '';
490
+ const hasApiKey = apiKey.length > 0;
491
+ // Mask API key: show first 4 and last 4 chars
492
+ const maskApiKey = (key) => {
493
+ if (key.length <= 12)
494
+ return key.slice(0, 3) + '...' + key.slice(-3);
495
+ return key.slice(0, 6) + '...' + key.slice(-4);
496
+ };
497
+ // Update check runs for everyone (no account required), with a hard
498
+ // race-timeout so a slow registry never delays the banner.
499
+ const updateLines = [];
500
+ const updatePromise = Promise.race([
501
+ checkForUpdates(version).catch(() => null),
502
+ new Promise((resolve) => setTimeout(() => resolve(null), 2000)),
503
+ ]);
504
+ // Resolve the update check BEFORE composing the welcome lines — the
505
+ // previous order built welcomeLines with `...updateLines` (the array
506
+ // was empty at that point) and only populated updateLines afterwards,
507
+ // so the upgrade banner literally never rendered. Bug shipped before
508
+ // the scoped-package rename made the check return wrong data anyway.
509
+ const updateInfo = await updatePromise;
510
+ if (updateInfo?.updateAvailable) {
511
+ // Detect + OFFER (don't force) — the user applies it in-shell with
512
+ // /update. Auto-installing on every startup ran `npm i -g` without
513
+ // consent and could fail silently; making it user-initiated is clearer.
514
+ this.pendingUpdate = updateInfo;
515
+ updateLines.push(chalk.cyan(' ⬆ ') +
516
+ chalk.dim('Update available: ') +
517
+ chalk.yellow(`v${updateInfo.current}`) +
518
+ chalk.dim(' → ') +
519
+ chalk.green(`v${updateInfo.latest}`) +
520
+ chalk.dim(' · type ') + chalk.hex('#ffb142')('/update') + chalk.dim(' to upgrade'));
521
+ }
522
+ // Clean, minimal welcome — a sparkle + the essentials in a rounded box,
523
+ // mirroring Claude Code. The pure composeWelcomeLines() is the contract for
524
+ // WHICH lines appear; here we draw the same box with brand colour.
525
+ const flare = chalk.hex('#ff6a1f');
526
+ const wire = chalk.hex('#3a362e');
527
+ const keyStatus = resolveKeyMode();
528
+ const body = welcomeBodyLines({
529
+ hasApiKey,
530
+ maskedKey: hasApiKey ? maskApiKey(apiKey) : '',
531
+ model: this.profileConfig.model,
532
+ provider: this.profileConfig.provider,
533
+ cwd: this.workingDir,
534
+ keyMode: keyStatus.mode,
535
+ keyModeLine: keyModeLine(keyStatus),
536
+ version: `v${version}`,
537
+ });
538
+ const boxed = roundedBox(body, (cell) => cell.replace('✻', flare('✻')), (s) => wire(s));
539
+ const welcomeContent = ['', ...updateLines, ...boxed, ''].join('\n');
540
+ // Use renderer event system instead of direct stdout writes
541
+ renderer.addEvent('banner', welcomeContent);
542
+ // Update renderer meta with model info
543
+ this.promptController?.setModelContext({
544
+ model: this.profileConfig.model,
545
+ provider: this.profileConfig.provider,
546
+ });
547
+ }
548
+ /**
549
+ * Kick off `npm install -g <pkg>@latest` in a background process. When it
550
+ * completes, surface a renderer event so the user sees the result without
551
+ * any blocking. The running CLI keeps the old code — the new version is
552
+ * picked up on next launch.
553
+ */
554
+ /**
555
+ * /update — re-check npm for a newer version (so it works on demand, not
556
+ * just from the startup notice) and, if one exists, upgrade in-shell. The
557
+ * install runs in the background and the new version takes effect on the
558
+ * next launch (a running Node process can't hot-swap its own global pkg).
559
+ */
560
+ async handleUpdateCommand() {
561
+ const renderer = this.promptController?.getRenderer();
562
+ this.promptController?.setStatusMessage('Checking npm for updates…');
563
+ const info = await checkForUpdates(getVersion(), true).catch(() => null); // force a fresh check
564
+ this.promptController?.setStatusMessage(null);
565
+ if (!info) {
566
+ renderer?.addEvent('system', chalk.dim('Could not reach npm to check for updates. Try again, or run: npm i -g @trenchwork/coder@latest'));
567
+ return;
568
+ }
569
+ if (!info.updateAvailable) {
570
+ renderer?.addEvent('system', chalk.dim(`You're on the latest version (v${info.current}).`));
571
+ this.pendingUpdate = null;
572
+ return;
573
+ }
574
+ renderer?.addEvent('system', chalk.cyan('⬆ ') + chalk.dim('Updating ') + chalk.yellow(`v${info.current}`) +
575
+ chalk.dim(' → ') + chalk.green(`v${info.latest}`) + chalk.dim('…'));
576
+ this.pendingUpdate = null;
577
+ this.runBackgroundUpdate(info);
578
+ }
579
+ runBackgroundUpdate(info) {
580
+ const renderer = this.promptController?.getRenderer();
581
+ void performBackgroundUpdate(info, (msg) => {
582
+ try {
583
+ renderer?.addEvent('system', msg);
584
+ }
585
+ catch { /* ignore */ }
586
+ }).then((res) => {
587
+ if (!res.started)
588
+ return;
589
+ try {
590
+ renderer?.addEvent('system', chalk.green(`✓ Update installer launched for v${info.latest}. `) +
591
+ chalk.dim('Exit and reopen the CLI to use the new version.'));
592
+ }
593
+ catch { /* ignore */ }
594
+ }).catch(() => { });
595
+ }
596
+ /**
597
+ * Set up handler for sudo password prompts from bash tool execution.
598
+ * When a sudo command needs a password, this prompts the user securely.
599
+ */
600
+ sudoPasswordHandler = null;
601
+ setupSudoPasswordHandler() {
602
+ this.sudoPasswordHandler = async () => {
603
+ const renderer = this.promptController?.getRenderer();
604
+ if (!renderer) {
605
+ provideSudoPassword(null);
606
+ return;
607
+ }
608
+ try {
609
+ // Show password prompt
610
+ renderer.addEvent('system', chalk.yellow('Sudo password required'));
611
+ renderer.setSecretMode(true);
612
+ renderer.clearBuffer();
613
+ // Capture password input
614
+ const password = await renderer.captureInput({ allowEmpty: false, trim: true, resetBuffer: true });
615
+ // Hide password mode
616
+ renderer.setSecretMode(false);
617
+ if (password) {
618
+ provideSudoPassword(password);
619
+ renderer.addEvent('system', chalk.green('✓ Password provided'));
620
+ }
621
+ else {
622
+ provideSudoPassword(null);
623
+ renderer.addEvent('system', chalk.yellow('Sudo cancelled'));
624
+ }
625
+ }
626
+ catch (error) {
627
+ renderer.setSecretMode(false);
628
+ provideSudoPassword(null);
629
+ reportStatus('Password prompt cancelled');
630
+ }
631
+ };
632
+ onSudoPasswordNeeded(this.sudoPasswordHandler);
633
+ }
634
+ cleanupSudoPasswordHandler() {
635
+ if (this.sudoPasswordHandler) {
636
+ offSudoPasswordNeeded(this.sudoPasswordHandler);
637
+ this.sudoPasswordHandler = null;
638
+ }
639
+ }
640
+ applyDebugState(enabled, statusMessage) {
641
+ this.debugEnabled = enabled;
642
+ setDebugMode(enabled);
643
+ this.promptController?.setDebugMode(enabled);
644
+ // Show transient status message instead of chat banner
645
+ if (statusMessage) {
646
+ this.promptController?.setStatusMessage(statusMessage);
647
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
648
+ }
649
+ }
650
+ describeEventForDebug(event) {
651
+ switch (event.type) {
652
+ case 'message.start':
653
+ return 'message.start';
654
+ case 'message.delta': {
655
+ const snippet = debugSnippet(event.content);
656
+ return snippet ? `message.delta → ${snippet}` : 'message.delta (empty)';
657
+ }
658
+ case 'message.complete': {
659
+ const snippet = debugSnippet(event.content);
660
+ return snippet
661
+ ? `message.complete → ${snippet} (${event.elapsedMs}ms)`
662
+ : `message.complete (${event.elapsedMs}ms)`;
663
+ }
664
+ case 'tool.start':
665
+ return `tool.start ${event.toolName}`;
666
+ case 'tool.complete': {
667
+ const snippet = debugSnippet(event.result);
668
+ return snippet
669
+ ? `tool.complete ${event.toolName} → ${snippet}`
670
+ : `tool.complete ${event.toolName}`;
671
+ }
672
+ case 'tool.error':
673
+ return `tool.error ${event.toolName} → ${event.error}`;
674
+ case 'edit.explanation': {
675
+ const snippet = debugSnippet(event.content);
676
+ return snippet ? `edit.explanation → ${snippet}` : 'edit.explanation';
677
+ }
678
+ case 'error':
679
+ return `error → ${event.error}`;
680
+ case 'usage': {
681
+ const parts = [];
682
+ if (event.inputTokens != null)
683
+ parts.push(`in:${event.inputTokens}`);
684
+ if (event.outputTokens != null)
685
+ parts.push(`out:${event.outputTokens}`);
686
+ if (event.totalTokens != null)
687
+ parts.push(`total:${event.totalTokens}`);
688
+ return `usage ${parts.length ? parts.join(', ') : '(no tokens)'}`;
689
+ }
690
+ default:
691
+ return event.type;
692
+ }
693
+ }
694
+ handleDebugCommand(arg) {
695
+ const normalized = arg?.toLowerCase();
696
+ // /debug alone - toggle
697
+ if (!normalized) {
698
+ const targetState = !this.debugEnabled;
699
+ this.applyDebugState(targetState, `Debug ${targetState ? 'on' : 'off'}`);
700
+ return true;
701
+ }
702
+ // /debug status - show current state
703
+ if (normalized === 'status') {
704
+ this.promptController?.setStatusMessage(`Debug is ${this.debugEnabled ? 'on' : 'off'}`);
705
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
706
+ return true;
707
+ }
708
+ // /debug on|enable
709
+ if (normalized === 'on' || normalized === 'enable') {
710
+ if (this.debugEnabled) {
711
+ this.promptController?.setStatusMessage('Debug already on');
712
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
713
+ return true;
714
+ }
715
+ this.applyDebugState(true, 'Debug on');
716
+ return true;
717
+ }
718
+ // /debug off|disable
719
+ if (normalized === 'off' || normalized === 'disable') {
720
+ if (!this.debugEnabled) {
721
+ this.promptController?.setStatusMessage('Debug already off');
722
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
723
+ return true;
724
+ }
725
+ this.applyDebugState(false, 'Debug off');
726
+ return true;
727
+ }
728
+ // Invalid argument
729
+ this.promptController?.setStatusMessage(`Invalid: /debug ${arg}. Use on|off|status`);
730
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2500);
731
+ return true;
732
+ }
733
+ /**
734
+ * Synthesize a user-facing response from reasoning content when the model
735
+ * provides reasoning but no actual response (common with deepseek-v4-pro).
736
+ * Extracts key conclusions and formats them as a concise response.
737
+ */
738
+ synthesizeFromReasoning(reasoning) {
739
+ if (!reasoning || reasoning.trim().length < 50) {
740
+ return null;
741
+ }
742
+ // Filter out internal meta-reasoning patterns that shouldn't be shown to user
743
+ const metaPatterns = [
744
+ /according to the rules?:?/gi,
745
+ /let me (?:use|search|look|check|find|think|analyze)/gi,
746
+ /I (?:should|need to|will|can|must) (?:use|search|look|check|find)/gi,
747
+ /⚡\s*Executing\.*/gi,
748
+ /use web\s?search/gi,
749
+ /for (?:non-)?coding (?:questions|tasks)/gi,
750
+ /answer (?:directly )?from knowledge/gi,
751
+ /this is a (?:general knowledge|coding|security)/gi,
752
+ /the user (?:is asking|wants|might be)/gi,
753
+ /however,? (?:the user|I|we)/gi,
754
+ /(?:first|next),? (?:I should|let me|I need)/gi,
755
+ ];
756
+ let filtered = reasoning;
757
+ for (const pattern of metaPatterns) {
758
+ filtered = filtered.replace(pattern, '');
759
+ }
760
+ // Split into sentences
761
+ const sentences = filtered
762
+ .split(/[.!?\n]+/)
763
+ .map(s => s.trim())
764
+ .filter(s => s.length > 20 && !/^[•\-–—*]/.test(s)); // Skip bullets and short fragments
765
+ if (sentences.length === 0) {
766
+ return null;
767
+ }
768
+ // Look for actual content (not process descriptions)
769
+ const contentPatterns = [
770
+ /(?:refers? to|involves?|relates? to|is about|concerns?)/i,
771
+ /(?:scandal|deal|agreement|proposal|plan|policy)/i,
772
+ /(?:Trump|Biden|Ukraine|Russia|president|congress)/i,
773
+ /(?:the (?:main|key|primary)|importantly)/i,
774
+ ];
775
+ const contentSentences = [];
776
+ for (const sentence of sentences) {
777
+ // Skip sentences that are clearly meta-reasoning
778
+ if (/^(?:so|therefore|thus|hence|accordingly)/i.test(sentence))
779
+ continue;
780
+ if (/(?:I should|let me|I will|I need|I can)/i.test(sentence))
781
+ continue;
782
+ for (const pattern of contentPatterns) {
783
+ if (pattern.test(sentence)) {
784
+ contentSentences.push(sentence);
785
+ break;
786
+ }
787
+ }
788
+ }
789
+ // Use content sentences if found, otherwise take last few sentences (often conclusions)
790
+ const useSentences = contentSentences.length > 0
791
+ ? contentSentences.slice(0, 3)
792
+ : sentences.slice(-3);
793
+ if (useSentences.length === 0) {
794
+ return null;
795
+ }
796
+ const response = useSentences.join('. ').replace(/\.{2,}/g, '.').trim();
797
+ // Don't prefix with "Based on my analysis" - just return clean content
798
+ return response.endsWith('.') ? response : response + '.';
799
+ }
800
+ async runLocalCommand(command) {
801
+ const renderer = this.promptController?.getRenderer();
802
+ if (!command) {
803
+ this.promptController?.setStatusMessage('Usage: /bash <command>');
804
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2500);
805
+ return;
806
+ }
807
+ this.promptController?.setStatusMessage(`bash: ${command}`);
808
+ try {
809
+ const { stdout: out, stderr } = await exec(command, {
810
+ cwd: this.workingDir,
811
+ maxBuffer: 4 * 1024 * 1024,
812
+ });
813
+ const output = [out, stderr].filter(Boolean).join('').trim() || '(no output)';
814
+ renderer?.addEvent('tool', `$ ${command}\n${output}`);
815
+ }
816
+ catch (error) {
817
+ const err = error;
818
+ const output = [err.stdout, err.stderr, err.message].filter(Boolean).join('\n').trim();
819
+ renderer?.addEvent('error', `$ ${command}\n${output || 'command failed'}`);
820
+ }
821
+ finally {
822
+ this.promptController?.setStatusMessage(null);
823
+ }
824
+ }
825
+ handleSlashCommand(command) {
826
+ const trimmed = command.trim();
827
+ const lower = trimmed.toLowerCase();
828
+ // /model and /secrets were removed: Trenchwork is locked to deepseek-v4-pro
829
+ // on max thought (no model switching), and /key is the one key you set.
830
+ // Handle /key — set your own DeepSeek OR Tavily API key. Routed by prefix:
831
+ // `sk-…` → DeepSeek (the model), `tvly-…` → Tavily (web search). Explicit
832
+ // `/key tavily <k>` / `/key deepseek <k>` also work. Bring-your-own-key is
833
+ // the model; both are stored in the OS-permission secret store.
834
+ if (lower === '/key' || lower.startsWith('/key ')) {
835
+ const renderer = this.promptController?.getRenderer();
836
+ const arg = trimmed.slice('/key'.length).trim();
837
+ const entry = classifyKeyEntry(arg);
838
+ if (entry) {
839
+ try {
840
+ setSecretValue(entry.id, entry.value);
841
+ const label = getSecretDefinition(entry.id)?.label ?? entry.id;
842
+ renderer?.addEvent('system', chalk.green(`✓ ${label} saved`));
843
+ }
844
+ catch (error) {
845
+ const msg = error instanceof Error ? error.message : String(error);
846
+ renderer?.addEvent('system', chalk.red(`✗ Failed: ${msg}`));
847
+ }
848
+ }
849
+ else {
850
+ renderer?.addEvent('system', chalk.yellow('Usage: /key sk-… (DeepSeek) or /key tvly-… (Tavily web search)'));
851
+ }
852
+ return true;
853
+ }
854
+ // /account — show the active key source (hosted vs your own) and switch
855
+ // between them. `/account own` forces your own keys even while signed in;
856
+ // `/account hosted` returns to hosted. Hosted keys come from sign-in
857
+ // (server-side, never baked into this client) — see core/hostedAuth.ts.
858
+ if (lower === '/account' || lower.startsWith('/account ')) {
859
+ const r = this.promptController?.getRenderer();
860
+ const arg = trimmed.slice('/account'.length).trim().toLowerCase();
861
+ if (arg === 'own')
862
+ setPreferOwnKeys(true);
863
+ else if (arg === 'hosted')
864
+ setPreferOwnKeys(false);
865
+ if (arg === 'own' || arg === 'hosted')
866
+ void this.showWelcome(); // banner reflects the switch
867
+ r?.addEvent('system', this.accountStatusText(resolveKeyMode()));
868
+ return true;
869
+ }
870
+ // /login — Google sign-in via ero.solar (loopback OAuth) to unlock hosted keys.
871
+ if (lower === '/login' || lower === '/signin') {
872
+ void this.handleLogin();
873
+ return true;
874
+ }
875
+ // /logout — drop the hosted session (back to your own keys, or none).
876
+ if (lower === '/logout' || lower === '/signout') {
877
+ clearHostedSession();
878
+ this.promptController?.getRenderer()?.addEvent('system', chalk.green('✓ Signed out — using your own keys.'));
879
+ void this.showWelcome();
880
+ return true;
881
+ }
882
+ // /update — check npm for a newer version and upgrade in-shell.
883
+ if (lower === '/update' || lower === '/upgrade') {
884
+ void this.handleUpdateCommand();
885
+ return true;
886
+ }
887
+ if (lower === '/help' || lower === '/h' || lower === '/?') {
888
+ this.showHelp();
889
+ return true;
890
+ }
891
+ if (lower === '/clear' || lower === '/c') {
892
+ this.pendingPrompts = [];
893
+ const r = this.promptController?.getRenderer();
894
+ r?.setFollowUpQueueMode(false);
895
+ r?.setQueuedPrompts([]);
896
+ this.promptController?.clearScreen();
897
+ void this.showWelcome();
898
+ return true;
899
+ }
900
+ if (lower.startsWith('/bash') || lower.startsWith('/sh ')) {
901
+ const cmd = trimmed.replace(/^\/(bash|sh)\s*/i, '').trim();
902
+ void this.runLocalCommand(cmd);
903
+ return true;
904
+ }
905
+ if (lower === '/exit' || lower === '/quit' || lower === '/q') {
906
+ this.handleExit();
907
+ return true;
908
+ }
909
+ // Keyboard shortcuts help
910
+ if (lower === '/keys' || lower === '/shortcuts' || lower === '/kb') {
911
+ this.showKeyboardShortcuts();
912
+ return true;
913
+ }
914
+ // /resume — pick a saved conversation and restore its full history into
915
+ // context (the agent continues where it left off).
916
+ if (lower === '/resume' || lower === '/sessions') {
917
+ this.handleResume();
918
+ return true;
919
+ }
920
+ // /context — show how much of the model's context window is in use.
921
+ if (lower === '/context' || lower === '/usage') {
922
+ this.showContext();
923
+ return true;
924
+ }
925
+ // /cost — DeepSeek tokens + Tavily searches consumed (this session + all
926
+ // time), and the hosted free-pool reference. Account-wide remaining is a
927
+ // backend number shown in the ero.solar portal.
928
+ if (lower === '/cost' || lower === '/spend') {
929
+ this.showUsage();
930
+ return true;
931
+ }
932
+ // /diff — review the files the agent changed this run, as colored diffs.
933
+ if (lower === '/diff' || lower === '/changes') {
934
+ this.showDiff();
935
+ return true;
936
+ }
937
+ // /rewind — restore the files changed this run to their prior state
938
+ // (two-step: preview, then `/rewind confirm`).
939
+ if (lower === '/rewind' || lower.startsWith('/rewind ') || lower === '/revert' || lower.startsWith('/revert ')) {
940
+ this.handleRewind(trimmed.split(/\s+/).slice(1).join(' '));
941
+ return true;
942
+ }
943
+ // Everything is on by default for max performance — there are no toggles.
944
+ // Ultracode + max thought, the adversarial verifier, and auto-continue all
945
+ // run under the hood. The /auto, /adversarial, /debug, /ultracode, /model,
946
+ // /secrets, /pin, and /email commands were removed; /key is the one knob.
947
+ return false;
948
+ }
949
+ /**
950
+ * Switch model silently without writing to chat.
951
+ * Accepts formats: "provider", "provider model", "provider/model", or "model"
952
+ * Updates status bar to show new model.
953
+ */
954
+ async switchModel(arg) {
955
+ // Ensure we have provider info
956
+ if (!this.cachedProviders) {
957
+ await this.fetchProviders();
958
+ }
959
+ const providers = this.cachedProviders || [];
960
+ const configuredProviders = getConfiguredProviders();
961
+ let targetProvider = null;
962
+ let targetModel = null;
963
+ // Parse argument: could be "provider model", "provider/model", "provider", or just "model"
964
+ // Check for space-separated format first: "openai o1-pro"
965
+ const parts = arg.split(/[\s/]+/);
966
+ if (parts.length >= 2) {
967
+ // Try first part as provider
968
+ const providerMatch = this.matchProvider(parts[0] || '');
969
+ if (providerMatch) {
970
+ targetProvider = providerMatch;
971
+ targetModel = parts.slice(1).join('/'); // Rest is model (handle models with slashes)
972
+ }
973
+ else {
974
+ // First part isn't a provider, treat whole arg as model name
975
+ const inferredProvider = this.inferProviderFromModel(arg.replace(/\s+/g, '-'));
976
+ if (inferredProvider) {
977
+ targetProvider = inferredProvider;
978
+ targetModel = arg.replace(/\s+/g, '-');
979
+ }
980
+ }
981
+ }
982
+ else {
983
+ // Single token - could be provider or model
984
+ const matched = this.matchProvider(arg);
985
+ if (matched) {
986
+ targetProvider = matched;
987
+ // Use provider's best model
988
+ const providerStatus = providers.find(p => p.provider === targetProvider);
989
+ targetModel = providerStatus?.latestModel || null;
990
+ }
991
+ else {
992
+ // Assume it's a model name - try to infer provider from model prefix
993
+ const inferredProvider = this.inferProviderFromModel(arg);
994
+ if (inferredProvider) {
995
+ targetProvider = inferredProvider;
996
+ targetModel = arg;
997
+ }
998
+ }
999
+ }
1000
+ // Validate we have a valid provider
1001
+ if (!targetProvider) {
1002
+ // Silent error - just flash status briefly
1003
+ this.promptController?.setStatusMessage(`Unknown: ${arg}`);
1004
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
1005
+ return;
1006
+ }
1007
+ // Check provider is configured
1008
+ const providerInfo = configuredProviders.find(p => p.id === targetProvider);
1009
+ if (!providerInfo) {
1010
+ // Provider not configured - offer to set up API key
1011
+ const secretMap = {
1012
+ 'deepseek': 'DEEPSEEK_API_KEY',
1013
+ };
1014
+ const secretId = secretMap[targetProvider];
1015
+ if (secretId) {
1016
+ this.promptController?.setStatusMessage(`${targetProvider} needs API key - setting up...`);
1017
+ // Store the pending model switch to complete after secret is set
1018
+ this.pendingModelSwitch = { provider: targetProvider, model: targetModel };
1019
+ setTimeout(() => this.promptForSecret(secretId), 500);
1020
+ return;
1021
+ }
1022
+ // Provider not supported
1023
+ this.promptController?.setStatusMessage(`${targetProvider} not available - only DeepSeek is supported`);
1024
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
1025
+ return;
1026
+ }
1027
+ // Get model if not specified
1028
+ if (!targetModel) {
1029
+ const providerStatus = providers.find(p => p.provider === targetProvider);
1030
+ targetModel = providerStatus?.latestModel || providerInfo.latestModel;
1031
+ }
1032
+ // Save preference and update config
1033
+ saveModelPreference(this.profile, {
1034
+ provider: targetProvider,
1035
+ model: targetModel,
1036
+ });
1037
+ // Update local config
1038
+ this.profileConfig = {
1039
+ ...this.profileConfig,
1040
+ provider: targetProvider,
1041
+ model: targetModel,
1042
+ };
1043
+ // Update controller's model
1044
+ await this.controller.switchModel({
1045
+ provider: targetProvider,
1046
+ model: targetModel,
1047
+ });
1048
+ // Update status bar - this displays the model below the chat box
1049
+ this.promptController?.setModelContext({
1050
+ model: targetModel,
1051
+ provider: targetProvider,
1052
+ });
1053
+ // Silent success - no chat output, just status bar update
1054
+ }
1055
+ /**
1056
+ * Match user input to a provider ID (fuzzy matching)
1057
+ */
1058
+ matchProvider(input) {
1059
+ const lower = input.toLowerCase();
1060
+ const providers = getConfiguredProviders();
1061
+ // Exact match
1062
+ const exact = providers.find(p => p.id === lower || p.name.toLowerCase() === lower);
1063
+ if (exact)
1064
+ return exact.id;
1065
+ // Prefix match
1066
+ const prefix = providers.find(p => p.id.startsWith(lower) || p.name.toLowerCase().startsWith(lower));
1067
+ if (prefix)
1068
+ return prefix.id;
1069
+ // Alias matching
1070
+ const aliases = {
1071
+ 'ds': 'deepseek',
1072
+ 'deep': 'deepseek',
1073
+ };
1074
+ if (aliases[lower]) {
1075
+ const aliased = providers.find(p => p.id === aliases[lower]);
1076
+ if (aliased)
1077
+ return aliased.id;
1078
+ }
1079
+ return null;
1080
+ }
1081
+ /**
1082
+ * Infer provider from model name
1083
+ */
1084
+ inferProviderFromModel(model) {
1085
+ const lower = model.toLowerCase();
1086
+ if (lower.startsWith('deepseek')) {
1087
+ return 'deepseek';
1088
+ }
1089
+ return null;
1090
+ }
1091
+ /**
1092
+ * Show interactive model picker menu (Claude Code style).
1093
+ * Auto-discovers latest models from each provider's API.
1094
+ * Uses arrow key navigation with inline panel display.
1095
+ */
1096
+ showModelMenu() {
1097
+ if (!this.promptController?.supportsInlinePanel()) {
1098
+ this.promptController?.setStatusMessage('Use /model <provider> <model> to switch');
1099
+ setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
1100
+ return;
1101
+ }
1102
+ // Show loading indicator
1103
+ this.promptController?.setStatusMessage('Discovering models...');
1104
+ // Fetch latest models from APIs
1105
+ void this.fetchAndShowModelMenu();
1106
+ }
1107
+ /**
1108
+ * Fetch models from provider APIs and show the interactive menu.
1109
+ */
1110
+ async fetchAndShowModelMenu() {
1111
+ try {
1112
+ // Get provider status and cached models
1113
+ const allProviders = getProvidersStatus();
1114
+ const cachedModels = getCachedDiscoveredModels();
1115
+ const currentModel = this.profileConfig.model;
1116
+ const currentProvider = this.profileConfig.provider;
1117
+ // Try to get fresh models from configured providers (with short timeout)
1118
+ let freshStatus = [];
1119
+ try {
1120
+ freshStatus = await Promise.race([
1121
+ quickCheckProviders(),
1122
+ new Promise((resolve) => setTimeout(() => resolve([]), 3000))
1123
+ ]);
1124
+ }
1125
+ catch {
1126
+ // Use cached data on error
1127
+ }
1128
+ // Build menu items - group by provider, show models
1129
+ const menuItems = [];
1130
+ for (const provider of allProviders) {
1131
+ // Get models for this provider
1132
+ const providerCachedModels = cachedModels.filter(m => m.provider === provider.id);
1133
+ const freshProvider = freshStatus.find(s => s.provider === provider.id);
1134
+ // Collect model IDs
1135
+ let modelIds = [];
1136
+ // Add fresh latest model if available
1137
+ if (freshProvider?.available && freshProvider.latestModel) {
1138
+ modelIds.push(freshProvider.latestModel);
1139
+ }
1140
+ // Add cached models
1141
+ modelIds.push(...providerCachedModels.map(m => m.id));
1142
+ // Add provider's default model
1143
+ if (provider.latestModel && !modelIds.includes(provider.latestModel)) {
1144
+ modelIds.push(provider.latestModel);
1145
+ }
1146
+ // Remove duplicates and sort by priority (best first)
1147
+ modelIds = [...new Set(modelIds)];
1148
+ modelIds = sortModelsByPriority(provider.id, modelIds);
1149
+ // Limit to top 3 models per provider
1150
+ const topModels = modelIds.slice(0, 3);
1151
+ if (!provider.configured) {
1152
+ // Show unconfigured provider as single disabled item
1153
+ menuItems.push({
1154
+ id: `${provider.id}:setup`,
1155
+ label: `${provider.name}`,
1156
+ description: `(${provider.envVar} not set - select to configure)`,
1157
+ category: provider.id,
1158
+ isActive: false,
1159
+ disabled: false, // Allow selection to configure
1160
+ });
1161
+ }
1162
+ else if (topModels.length === 0) {
1163
+ // No models found - show provider with default
1164
+ menuItems.push({
1165
+ id: `${provider.id}:${provider.latestModel}`,
1166
+ label: `${provider.name} › ${provider.latestModel}`,
1167
+ description: 'default',
1168
+ category: provider.id,
1169
+ isActive: provider.id === currentProvider && provider.latestModel === currentModel,
1170
+ disabled: false,
1171
+ });
1172
+ }
1173
+ else {
1174
+ // Show each model as selectable item
1175
+ for (const modelId of topModels) {
1176
+ const isCurrentModel = provider.id === currentProvider && modelId === currentModel;
1177
+ const modelLabel = this.formatModelLabel(modelId);
1178
+ menuItems.push({
1179
+ id: `${provider.id}:${modelId}`,
1180
+ label: `${provider.name} › ${modelLabel}`,
1181
+ description: isCurrentModel ? '(current)' : '',
1182
+ category: provider.id,
1183
+ isActive: isCurrentModel,
1184
+ disabled: false,
1185
+ });
1186
+ }
1187
+ }
1188
+ }
1189
+ // Clear loading message
1190
+ this.promptController?.setStatusMessage(null);
1191
+ // Show the interactive menu
1192
+ this.promptController?.setMenu(menuItems, { title: 'Select Model' }, (selected) => {
1193
+ if (selected) {
1194
+ // Parse provider:model format
1195
+ const [providerId, ...modelParts] = selected.id.split(':');
1196
+ const modelId = modelParts.join(':');
1197
+ if (modelId === 'setup') {
1198
+ // Configure provider API key
1199
+ const secretMap = {
1200
+ 'deepseek': 'DEEPSEEK_API_KEY',
1201
+ };
1202
+ const secretId = secretMap[providerId ?? ''];
1203
+ if (secretId) {
1204
+ this.promptForSecret(secretId);
1205
+ }
1206
+ }
1207
+ else {
1208
+ // Switch to selected model
1209
+ void this.switchModel(`${providerId} ${modelId}`);
1210
+ }
1211
+ }
1212
+ });
1213
+ }
1214
+ catch (error) {
1215
+ this.promptController?.setStatusMessage('Failed to load models');
1216
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
1217
+ }
1218
+ }
1219
+ /**
1220
+ * Format model ID for display (shorten long IDs).
1221
+ */
1222
+ formatModelLabel(modelId) {
1223
+ let label = modelId
1224
+ .replace(/^deepseek-/, 'DeepSeek ');
1225
+ if (label.length > 30) {
1226
+ label = label.slice(0, 27) + '...';
1227
+ }
1228
+ return label;
1229
+ }
1230
+ showSecrets() {
1231
+ const secrets = listSecretDefinitions();
1232
+ if (!this.promptController?.supportsInlinePanel()) {
1233
+ // Fallback for non-TTY - use status message
1234
+ const setCount = secrets.filter(s => !!process.env[s.envVar]).length;
1235
+ this.promptController?.setStatusMessage(`API Keys: ${setCount}/${secrets.length} configured`);
1236
+ setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
1237
+ return;
1238
+ }
1239
+ // Build interactive menu items
1240
+ const menuItems = secrets.map(secret => {
1241
+ const isSet = !!process.env[secret.envVar];
1242
+ const statusIcon = isSet ? '✓' : '✗';
1243
+ const providers = secret.providers?.length ? ` (${secret.providers.join(', ')})` : '';
1244
+ return {
1245
+ id: secret.id,
1246
+ label: `${statusIcon} ${secret.envVar}`,
1247
+ description: isSet ? 'configured' + providers : 'not set' + providers,
1248
+ isActive: isSet,
1249
+ disabled: false,
1250
+ };
1251
+ });
1252
+ // Show the interactive menu
1253
+ this.promptController.setMenu(menuItems, { title: 'API Keys — Select to Configure' }, (selected) => {
1254
+ if (selected) {
1255
+ // Start secret input for selected key
1256
+ this.promptForSecret(selected.id);
1257
+ }
1258
+ });
1259
+ }
1260
+ /**
1261
+ * Start interactive secret input flow.
1262
+ * If secretArg is provided, set only that secret.
1263
+ * Otherwise, prompt for all unset secrets.
1264
+ */
1265
+ async startSecretInput(secretArg) {
1266
+ const secrets = listSecretDefinitions();
1267
+ if (secretArg) {
1268
+ // Set a specific secret
1269
+ const upper = secretArg.toUpperCase();
1270
+ const secret = secrets.find(s => s.id === upper || s.envVar === upper);
1271
+ if (!secret) {
1272
+ this.promptController?.setStatusMessage(`Unknown secret: ${secretArg}`);
1273
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
1274
+ return;
1275
+ }
1276
+ this.promptForSecret(secret.id);
1277
+ return;
1278
+ }
1279
+ // Queue all unset secrets for input
1280
+ const unsetSecrets = secrets.filter(s => !getSecretValue(s.id));
1281
+ if (unsetSecrets.length === 0) {
1282
+ this.promptController?.setStatusMessage('All secrets configured');
1283
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
1284
+ return;
1285
+ }
1286
+ // Queue all unset secrets and start with the first one
1287
+ this.secretInputMode.queue = unsetSecrets.map(s => s.id);
1288
+ const first = this.secretInputMode.queue.shift();
1289
+ if (first) {
1290
+ this.promptForSecret(first);
1291
+ }
1292
+ }
1293
+ /**
1294
+ * Show prompt for a specific secret and enable secret input mode.
1295
+ */
1296
+ promptForSecret(secretId) {
1297
+ const secrets = listSecretDefinitions();
1298
+ const secret = secrets.find(s => s.id === secretId);
1299
+ if (!secret)
1300
+ return;
1301
+ // Show in inline panel (no chat output)
1302
+ if (this.promptController?.supportsInlinePanel()) {
1303
+ const lines = [
1304
+ chalk.bold.hex('#ece6da')(`Set ${secret.label}`),
1305
+ chalk.dim(secret.description),
1306
+ '',
1307
+ chalk.dim('Enter value (or press Enter to skip)'),
1308
+ ];
1309
+ this.promptController.setInlinePanel(lines);
1310
+ }
1311
+ // Enable secret input mode
1312
+ this.secretInputMode.active = true;
1313
+ this.secretInputMode.secretId = secretId;
1314
+ this.promptController?.setSecretMode(true);
1315
+ this.promptController?.setStatusMessage(`Enter ${secret.label}...`);
1316
+ }
1317
+ /**
1318
+ * Handle secret value submission.
1319
+ */
1320
+ handleSecretValue(value) {
1321
+ const secretId = this.secretInputMode.secretId;
1322
+ if (!secretId)
1323
+ return;
1324
+ // Disable secret mode and clear inline panel
1325
+ this.promptController?.setSecretMode(false);
1326
+ this.promptController?.clearInlinePanel();
1327
+ this.secretInputMode.active = false;
1328
+ this.secretInputMode.secretId = null;
1329
+ let savedSuccessfully = false;
1330
+ if (value.trim()) {
1331
+ try {
1332
+ setSecretValue(secretId, value.trim());
1333
+ this.promptController?.setStatusMessage(`${secretId} saved`);
1334
+ savedSuccessfully = true;
1335
+ }
1336
+ catch (error) {
1337
+ const msg = error instanceof Error ? error.message : 'Failed to save';
1338
+ this.promptController?.setStatusMessage(msg);
1339
+ }
1340
+ }
1341
+ else {
1342
+ this.promptController?.setStatusMessage(`Skipped ${secretId}`);
1343
+ }
1344
+ // Clear status after a moment
1345
+ setTimeout(() => this.promptController?.setStatusMessage(null), 1500);
1346
+ // Process next secret in queue if any
1347
+ if (this.secretInputMode.queue.length > 0) {
1348
+ const next = this.secretInputMode.queue.shift();
1349
+ if (next) {
1350
+ setTimeout(() => this.promptForSecret(next), 500);
1351
+ }
1352
+ return;
1353
+ }
1354
+ // Complete pending model switch if secret was saved successfully
1355
+ if (savedSuccessfully && this.pendingModelSwitch) {
1356
+ const { provider, model } = this.pendingModelSwitch;
1357
+ this.pendingModelSwitch = null;
1358
+ // Refresh provider cache and complete the switch
1359
+ setTimeout(async () => {
1360
+ await this.fetchProviders();
1361
+ await this.switchModel(model ? `${provider} ${model}` : provider);
1362
+ }, 500);
1363
+ }
1364
+ }
1365
+ /**
1366
+ * Snapshot the live conversation to the session store so /resume can
1367
+ * restore it later. Best-effort: a persistence failure must never break a
1368
+ * turn, so everything is wrapped. Skips empty/system-only histories so we
1369
+ * don't litter the picker with sessions that have no real exchange.
1370
+ */
1371
+ persistSessionSnapshot() {
1372
+ try {
1373
+ const messages = this.controller.getHistory();
1374
+ if (!messages.some((m) => m.role === 'user')) {
1375
+ return;
1376
+ }
1377
+ const summary = saveSessionSnapshot({
1378
+ id: this.sessionId,
1379
+ profile: this.profile,
1380
+ provider: this.profileConfig.provider,
1381
+ model: this.profileConfig.model,
1382
+ workspaceRoot: this.workingDir,
1383
+ messages,
1384
+ });
1385
+ this.sessionId = summary.id;
1386
+ }
1387
+ catch {
1388
+ // best-effort persistence — never interrupt the user's turn
1389
+ }
1390
+ }
1391
+ /**
1392
+ * /resume — present saved conversations newest-first and restore the
1393
+ * chosen one's full message history into the agent's context.
1394
+ */
1395
+ handleResume() {
1396
+ const renderer = this.promptController?.getRenderer();
1397
+ const sessions = listSessions().filter((s) => s.id !== this.sessionId && s.messageCount > 0);
1398
+ if (sessions.length === 0) {
1399
+ renderer?.addEvent('system', chalk.dim('No saved conversations to resume yet.'));
1400
+ return;
1401
+ }
1402
+ const items = sessions.slice(0, 25).map((s) => ({
1403
+ id: s.id,
1404
+ label: s.title,
1405
+ description: `${s.messageCount} msg · ${relativeTime(s.updatedAt)}`,
1406
+ }));
1407
+ this.promptController?.setMenu(items, { title: 'Resume a conversation' }, (selected) => {
1408
+ if (selected) {
1409
+ this.resumeSession(selected.id);
1410
+ }
1411
+ });
1412
+ }
1413
+ /**
1414
+ * Load a saved session by id, restore its history into the controller (and
1415
+ * thus the agent's context), and reprint the prior exchange so the user
1416
+ * sees where they left off.
1417
+ */
1418
+ resumeSession(id) {
1419
+ const renderer = this.promptController?.getRenderer();
1420
+ const stored = loadSessionById(id);
1421
+ if (!stored) {
1422
+ renderer?.addEvent('error', 'That conversation could not be loaded.');
1423
+ return;
1424
+ }
1425
+ this.controller.loadHistory(stored.messages);
1426
+ this.sessionId = stored.id;
1427
+ const restored = stored.messages.filter((m) => m.role === 'user' || m.role === 'assistant');
1428
+ renderer?.addEvent('system', chalk.dim(`Resumed "${stored.title}" — ${restored.length} message${restored.length === 1 ? '' : 's'} restored`));
1429
+ for (const m of stored.messages) {
1430
+ if (m.role === 'user') {
1431
+ renderer?.addUserHistoryItem(m.content);
1432
+ }
1433
+ else if (m.role === 'assistant' && m.content.trim()) {
1434
+ renderer?.addEvent('response', m.content);
1435
+ }
1436
+ }
1437
+ }
1438
+ /**
1439
+ * /context — a compact context-window usage panel. Uses the real model
1440
+ * window and the provider's last input-token count (falls back to a char/4
1441
+ * estimate, marked "~", before the first turn).
1442
+ */
1443
+ showContext() {
1444
+ if (!this.promptController?.supportsInlinePanel()) {
1445
+ this.promptController?.setStatusMessage('Use /context in interactive mode');
1446
+ setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
1447
+ return;
1448
+ }
1449
+ const model = this.profileConfig.model;
1450
+ const windowTokens = getModelContextInfo(model).contextWindow;
1451
+ const usage = computeContextUsage(this.controller.getHistory(), windowTokens, this.lastInputTokens);
1452
+ const label = (s) => chalk.hex('#ffb142')(s.padEnd(8));
1453
+ const dim = (s) => chalk.dim(s);
1454
+ const approx = usage.estimated ? '~' : '';
1455
+ const barWidth = 24;
1456
+ const filled = Math.min(barWidth, Math.round((usage.percentUsed / 100) * barWidth));
1457
+ const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
1458
+ const lines = [
1459
+ chalk.bold.hex('#ece6da')('Context') + dim(' (press any key to dismiss)'),
1460
+ '',
1461
+ dim(bar) + ' ' + chalk.hex('#ece6da')(`${usage.percentLeft}% context left`),
1462
+ '',
1463
+ label('Window') + dim(`${formatTokenCount(windowTokens)} tokens · ${model}`),
1464
+ label('Used') + dim(`${approx}${formatTokenCount(usage.usedTokens)} tokens (${usage.percentUsed}%)`),
1465
+ label('Free') + dim(`${formatTokenCount(usage.freeTokens)} tokens (${usage.percentLeft}%)`),
1466
+ '',
1467
+ dim(`System prompt ~${formatTokenCount(usage.systemTokens)} · conversation ~${formatTokenCount(usage.conversationTokens)} · ${usage.messageCount} messages`),
1468
+ ];
1469
+ this.promptController.setInlinePanel(lines);
1470
+ this.scheduleInlinePanelDismiss();
1471
+ }
1472
+ /** /cost — DeepSeek tokens + Tavily searches consumed (this install). */
1473
+ showUsage() {
1474
+ if (!this.promptController?.supportsInlinePanel()) {
1475
+ this.promptController?.setStatusMessage('Use /cost in interactive mode');
1476
+ setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
1477
+ return;
1478
+ }
1479
+ const { session, cumulative } = getUsage();
1480
+ const label = (s) => chalk.hex('#ffb142')(s.padEnd(9));
1481
+ const dim = (s) => chalk.dim(s);
1482
+ const ds = (u) => `${formatTokenCount(u.deepseekInputTokens)} in · ${formatTokenCount(u.deepseekOutputTokens)} out`;
1483
+ const lines = [
1484
+ chalk.bold.hex('#ece6da')('Usage') + dim(' (press any key to dismiss)'),
1485
+ '',
1486
+ label('DeepSeek') + dim(`${ds(cumulative)} · this session ${ds(session)}`),
1487
+ label('Tavily') + dim(`${cumulative.tavilySearches} searches · this session ${session.tavilySearches}`),
1488
+ '',
1489
+ dim(`Hosted free pool: Tavily ${TAVILY_MONTHLY_FREE.toLocaleString('en-US')}/mo + ${TAVILY_ONE_TIME_BONUS.toLocaleString('en-US')} one-time bonus.`),
1490
+ dim('Account-wide totals + remaining show in the ero.solar portal after sign-in.'),
1491
+ ];
1492
+ this.promptController.setInlinePanel(lines);
1493
+ this.scheduleInlinePanelDismiss();
1494
+ }
1495
+ /**
1496
+ * /diff — review every file the agent changed this run as a colored diff,
1497
+ * in a dismissable panel. Reads each file's original content from the change
1498
+ * tracker and its current content from disk; an empty tracker means nothing
1499
+ * changed since the last prompt.
1500
+ */
1501
+ showDiff() {
1502
+ const renderer = this.promptController?.getRenderer();
1503
+ const changed = getChangedFiles();
1504
+ if (changed.size === 0) {
1505
+ renderer?.addEvent('system', chalk.dim('No file changes in the last run.'));
1506
+ return;
1507
+ }
1508
+ if (!this.promptController?.supportsInlinePanel()) {
1509
+ this.promptController?.setStatusMessage(`${changed.size} file(s) changed this run`);
1510
+ setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
1511
+ return;
1512
+ }
1513
+ const items = [];
1514
+ for (const [absPath, record] of changed) {
1515
+ let current = '';
1516
+ let deleted = false;
1517
+ try {
1518
+ current = readFileSync(absPath, 'utf-8');
1519
+ }
1520
+ catch {
1521
+ deleted = true;
1522
+ }
1523
+ items.push({
1524
+ relPath: relative(this.workingDir, absPath) || absPath,
1525
+ previous: record.originalContent ?? '',
1526
+ current,
1527
+ existedBefore: record.existedBefore,
1528
+ deleted,
1529
+ });
1530
+ }
1531
+ const panel = renderChangePanel(items);
1532
+ const dim = (s) => chalk.dim(s);
1533
+ const lines = [
1534
+ chalk.bold.hex('#ece6da')('Changes') + dim(' (press any key to dismiss)'),
1535
+ '',
1536
+ ...panel.lines,
1537
+ ];
1538
+ this.promptController.setInlinePanel(lines);
1539
+ this.scheduleInlinePanelDismiss();
1540
+ }
1541
+ /**
1542
+ * /rewind — restore the files changed this run. Two-step: bare `/rewind`
1543
+ * previews what will be restored/deleted (kept in the transcript so it stays
1544
+ * visible while the user types the confirm); `/rewind confirm` performs the
1545
+ * revert via the change tracker. File-level only — the conversation is not
1546
+ * rewound, and the message says so by scope ("files … before this run").
1547
+ */
1548
+ handleRewind(arg) {
1549
+ const renderer = this.promptController?.getRenderer();
1550
+ const changed = getChangedFiles();
1551
+ if (changed.size === 0 || !hasChangesToRevert()) {
1552
+ renderer?.addEvent('system', chalk.dim('Nothing to rewind — no file changes this run.'));
1553
+ return;
1554
+ }
1555
+ if (arg.trim().toLowerCase() !== 'confirm') {
1556
+ const items = [...changed].map(([abs, rec]) => ({
1557
+ relPath: relative(this.workingDir, abs) || abs,
1558
+ existedBefore: rec.existedBefore,
1559
+ }));
1560
+ const lines = rewindPreviewLines(items);
1561
+ lines.forEach((line, i) => {
1562
+ const last = i === lines.length - 1;
1563
+ renderer?.addEvent('system', last ? chalk.hex('#ffb142')(line) : chalk.dim(line));
1564
+ });
1565
+ return;
1566
+ }
1567
+ let restored = 0;
1568
+ let deleted = 0;
1569
+ for (const [, rec] of changed) {
1570
+ if (rec.existedBefore && rec.originalContent !== null)
1571
+ restored += 1;
1572
+ else if (!rec.existedBefore)
1573
+ deleted += 1;
1574
+ }
1575
+ revertAllChanges(this.workingDir); // restores/deletes on disk + clears tracking
1576
+ renderer?.addEvent('system', chalk.green('✓ ' + rewindResultLine(restored, deleted)));
1577
+ }
1578
+ /** One-line summary of the active key source for /account. */
1579
+ accountStatusText(s) {
1580
+ if (s.mode === 'hosted') {
1581
+ return chalk.green(`Hosted keys · signed in as ${s.email}.`) +
1582
+ chalk.dim(` /account own to use your own · /logout to sign out.`);
1583
+ }
1584
+ if (s.mode === 'own') {
1585
+ return chalk.green(`Your own keys · DeepSeek${s.ownTavily ? ' + Tavily' : ''}.`) +
1586
+ chalk.dim(s.signedIn ? ` /account hosted to use hosted keys.` : ` /login to use hosted keys.`);
1587
+ }
1588
+ return chalk.yellow('No keys configured.') +
1589
+ chalk.dim(' /login for hosted keys, or set your own: /key sk-… (and /key tvly-…).');
1590
+ }
1591
+ /**
1592
+ * /login — Google sign-in via ero.solar. Opens the browser to the SSO URL and
1593
+ * runs a one-shot 127.0.0.1 loopback server that captures the redirect with
1594
+ * the short-lived token (see core/hostedAuth.ts). On success the CLI is on
1595
+ * hosted keys; no key ever touches this client.
1596
+ */
1597
+ async handleLogin() {
1598
+ const r = this.promptController?.getRenderer();
1599
+ const status = resolveKeyMode();
1600
+ if (status.signedIn) {
1601
+ r?.addEvent('system', chalk.green(`Already signed in as ${status.email}.`) +
1602
+ chalk.dim(' /logout to sign out · /account to switch key source.'));
1603
+ return;
1604
+ }
1605
+ r?.addEvent('system', chalk.dim('Opening ero.solar sign-in in your browser — finish there, then return here…'));
1606
+ const result = await loginViaLoopback({ open: (url) => this.openInBrowser(url) });
1607
+ if (result.ok && result.session) {
1608
+ r?.addEvent('system', chalk.green(`✓ Signed in as ${result.session.email} — using hosted keys.`));
1609
+ void this.showWelcome();
1610
+ }
1611
+ else {
1612
+ r?.addEvent('system', chalk.yellow(`Sign-in didn't complete: ${result.error ?? 'unknown error'}.`) +
1613
+ chalk.dim(' Retry /login, or use /key sk-… for your own key.'));
1614
+ }
1615
+ }
1616
+ /** Best-effort open a URL in the OS browser; also prints it as a fallback. */
1617
+ openInBrowser(url) {
1618
+ const opener = process.platform === 'darwin' ? 'open'
1619
+ : process.platform === 'win32' ? 'start ""'
1620
+ : 'xdg-open';
1621
+ // url is built by loginViaLoopback (no user input) and JSON-quoted, so the
1622
+ // `&` in the query string can't break out of the argument.
1623
+ childExec(`${opener} ${JSON.stringify(url)}`, () => { });
1624
+ this.promptController?.getRenderer()?.addEvent('system', chalk.dim(`If the browser didn't open: ${url}`));
1625
+ }
1626
+ showHelp() {
1627
+ if (!this.promptController?.supportsInlinePanel()) {
1628
+ this.promptController?.setStatusMessage('Help: /key sk-… (everything else is automatic)');
1629
+ setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
1630
+ return;
1631
+ }
1632
+ const cmd = (s) => chalk.hex('#ffb142')(s);
1633
+ const dim = (s) => chalk.dim(s);
1634
+ // One knob. Everything else (ultracode, max thought, the adversarial
1635
+ // verifier, auto-continue) is on by default for max performance — there
1636
+ // are no toggles to tune.
1637
+ const lines = [
1638
+ chalk.bold.hex('#ece6da')('Trenchwork Coder') + dim(' (press any key to dismiss)'),
1639
+ '',
1640
+ cmd('/login') + dim(' Sign in with Google (ero.solar) to use hosted keys'),
1641
+ cmd('/key sk-…') + dim(' Set your DeepSeek API key (required)'),
1642
+ cmd('/key tvly-…') + dim(' Set your Tavily key for web search (optional)'),
1643
+ cmd('/account') + dim(' Show / switch key source (hosted vs your own)'),
1644
+ cmd('/update') + dim(' Check npm and upgrade to the latest version'),
1645
+ cmd('/resume') + dim(' Restore a previous conversation'),
1646
+ cmd('/context') + dim(' Show context-window usage'),
1647
+ cmd('/cost') + dim(' DeepSeek tokens + Tavily searches consumed'),
1648
+ cmd('/diff') + dim(' Review changes made this run'),
1649
+ cmd('/rewind') + dim(' Undo this run\'s file changes'),
1650
+ '',
1651
+ dim('Prefixes: ') + cmd('@file') + dim(' attach · ') + cmd('!cmd') + dim(' run shell · ') + cmd('#note') + dim(' save to memory'),
1652
+ '',
1653
+ dim('Everything else runs automatically —'),
1654
+ dim('deepseek-v4-pro · max thought · ultracode · adversarial verifier, all on.'),
1655
+ dim('Shift+Tab cycles permission mode · Ctrl+D exits · ? for shortcuts'),
1656
+ ];
1657
+ this.promptController.setInlinePanel(lines);
1658
+ this.scheduleInlinePanelDismiss();
1659
+ }
1660
+ showKeyboardShortcuts() {
1661
+ if (!this.promptController?.supportsInlinePanel()) {
1662
+ this.promptController?.setStatusMessage('Use /keys in interactive mode');
1663
+ setTimeout(() => this.promptController?.setStatusMessage(null), 3000);
1664
+ return;
1665
+ }
1666
+ const kb = (key) => chalk.hex('#ffb142')(key);
1667
+ const desc = (text) => chalk.dim(text);
1668
+ // Only shortcuts the Ink Prompt (src/ui/ink/Prompt.tsx) actually
1669
+ // implements are listed — advertising keys the input handler ignores
1670
+ // would be a deceptive panel (Glasswing transparency).
1671
+ const lines = [
1672
+ chalk.bold.hex('#ece6da')('Keyboard Shortcuts') + chalk.dim(' (press any key to dismiss)'),
1673
+ '',
1674
+ chalk.hex('#cbf24e')('Navigation'),
1675
+ ` ${kb('Ctrl+A')} / ${kb('Home')} ${desc('Move to start of line')}`,
1676
+ ` ${kb('Ctrl+E')} / ${kb('End')} ${desc('Move to end of line')}`,
1677
+ ` ${kb('←')} / ${kb('→')} ${desc('Move cursor')}`,
1678
+ ` ${kb('↑')} / ${kb('↓')} ${desc('Prompt history (older / newer)')}`,
1679
+ ` ${kb('Ctrl+R')} ${desc('Reverse-search prompt history')}`,
1680
+ '',
1681
+ chalk.hex('#cbf24e')('Editing'),
1682
+ ` ${kb('Ctrl+U')} ${desc('Delete to start of line')}`,
1683
+ ` ${kb('Ctrl+W')} ${desc('Delete word backward')}`,
1684
+ ` ${kb('Ctrl+K')} ${desc('Delete to end of line')}`,
1685
+ '',
1686
+ chalk.hex('#cbf24e')('Modes'),
1687
+ ` ${kb('Shift+Tab')} ${desc('Cycle permission mode (default · accept edits · plan)')}`,
1688
+ ` ${kb('Ctrl+O')} ${desc('Expand the last truncated tool result')}`,
1689
+ '',
1690
+ chalk.hex('#cbf24e')('Completion'),
1691
+ ` ${kb('@')} ${desc('Autocomplete a file (↑/↓ · Tab/Enter); its content is inlined for the agent')}`,
1692
+ ` ${kb('/')} ${desc('Autocomplete a command (↑/↓ · Tab to complete; Enter runs it)')}`,
1693
+ '',
1694
+ chalk.hex('#cbf24e')('Control'),
1695
+ ` ${kb('Ctrl+C')} ${desc('Clear input / interrupt')}`,
1696
+ ` ${kb('Ctrl+D')} ${desc('Exit (when empty)')}`,
1697
+ ];
1698
+ this.promptController.setInlinePanel(lines);
1699
+ this.scheduleInlinePanelDismiss();
1700
+ }
1701
+ /**
1702
+ * Auto-dismiss inline panel after timeout or on next input.
1703
+ */
1704
+ inlinePanelDismissTimer = null;
1705
+ scheduleInlinePanelDismiss() {
1706
+ // Clear any existing timer
1707
+ if (this.inlinePanelDismissTimer) {
1708
+ clearTimeout(this.inlinePanelDismissTimer);
1709
+ }
1710
+ // Auto-dismiss after 8 seconds
1711
+ this.inlinePanelDismissTimer = setTimeout(() => {
1712
+ this.promptController?.clearInlinePanel();
1713
+ this.inlinePanelDismissTimer = null;
1714
+ }, 8000);
1715
+ }
1716
+ dismissInlinePanel() {
1717
+ if (this.inlinePanelDismissTimer) {
1718
+ clearTimeout(this.inlinePanelDismissTimer);
1719
+ this.inlinePanelDismissTimer = null;
1720
+ }
1721
+ this.promptController?.clearInlinePanel();
1722
+ }
1723
+ handleSubmit(text) {
1724
+ const trimmed = text.trim();
1725
+ // Handle secret input mode - capture the API key value
1726
+ if (this.secretInputMode.active && this.secretInputMode.secretId) {
1727
+ this.handleSecretValue(trimmed);
1728
+ return;
1729
+ }
1730
+ if (!trimmed) {
1731
+ return;
1732
+ }
1733
+ // Handle slash commands first - these don't go to the AI
1734
+ if (trimmed.startsWith('/')) {
1735
+ if (this.handleSlashCommand(trimmed)) {
1736
+ return;
1737
+ }
1738
+ // Unknown slash command - silent status flash, dismiss inline panel
1739
+ this.dismissInlinePanel();
1740
+ this.promptController?.setStatusMessage(`Unknown: ${trimmed.slice(0, 30)}`);
1741
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
1742
+ return;
1743
+ }
1744
+ // `!cmd` — bash mode (Claude Code parity): run the rest as a shell command
1745
+ // directly, no model round-trip. Same executor as /bash, via the leading
1746
+ // bang. Runs immediately (like a slash command), not queued behind the agent.
1747
+ if (trimmed.startsWith('!')) {
1748
+ this.dismissInlinePanel();
1749
+ void this.runLocalCommand(trimmed.slice(1).trim());
1750
+ return;
1751
+ }
1752
+ // `#note` — quick-capture a note to persistent project memory (Claude Code
1753
+ // parity), no model round-trip. Lands in .trenchwork/memory/ where the agent
1754
+ // reads it on later sessions.
1755
+ if (trimmed.startsWith('#')) {
1756
+ this.dismissInlinePanel();
1757
+ const note = trimmed.slice(1).trim();
1758
+ const r = this.promptController?.getRenderer();
1759
+ if (appendMemoryNote(this.workingDir, note))
1760
+ r?.addEvent('system', chalk.green('✓ Saved to memory'));
1761
+ else
1762
+ r?.addEvent('system', chalk.yellow('Usage: #<note to remember>'));
1763
+ return;
1764
+ }
1765
+ // Dismiss inline panel for regular user prompts
1766
+ this.dismissInlinePanel();
1767
+ // Live follow-up queue (Claude Code parity): a prompt typed while the agent
1768
+ // is working is accepted immediately into a transient queue (visible above
1769
+ // the input, *not* in permanent history). It is processed at the next turn
1770
+ // boundary (ASAP, before any outer auto-continue decides the original task
1771
+ // is "complete"). No polluting system banners.
1772
+ if (this.isProcessing) {
1773
+ this.pendingPrompts.push(trimmed);
1774
+ const renderer = this.promptController?.getRenderer();
1775
+ renderer?.setFollowUpQueueMode(true);
1776
+ renderer?.setQueuedPrompts(this.pendingPrompts.slice());
1777
+ return;
1778
+ }
1779
+ void this.processPrompt(trimmed);
1780
+ }
1781
+ async processPrompt(prompt) {
1782
+ if (this.isProcessing) {
1783
+ return;
1784
+ }
1785
+ // Start new run for file change tracking (enables /revert)
1786
+ startNewRun();
1787
+ // @-file mentions: inline the content of any `@path` the user referenced
1788
+ // so the agent gets it directly (the chat history still shows the raw
1789
+ // `@path` the user typed — only this agent-bound copy is expanded).
1790
+ const mentions = expandFileMentions(prompt, this.workingDir);
1791
+ const sanitizedPrompt = mentions.prompt;
1792
+ if (mentions.included.length > 0) {
1793
+ this.promptController?.getRenderer()?.addEvent('system', chalk.dim(`Included ${mentions.included.length} referenced file${mentions.included.length === 1 ? '' : 's'}: ${mentions.included.join(', ')}`));
1794
+ }
1795
+ // Store original prompt for auto-continuation (if not a continuation or auto-generated prompt)
1796
+ if (prompt !== 'continue' && !prompt.startsWith('IMPORTANT:')) {
1797
+ this.originalPromptForAutoContinue = prompt;
1798
+ // A fresh user prompt clears any prior interrupt state — this is new
1799
+ // work the user actually wants done.
1800
+ this.userInterruptedRun = false;
1801
+ // Fresh user request → start a new auto-continue turn budget + failure log.
1802
+ this.autoGovernor.reset();
1803
+ this.failureRegistry.reset();
1804
+ this.adversarialCorrectionCount = 0;
1805
+ // Pinned-prompt persistence removed per request — no longer
1806
+ // displayed above the chat box.
1807
+ }
1808
+ enterCriticalSection();
1809
+ this.isProcessing = true;
1810
+ this.currentResponseBuffer = '';
1811
+ this.finalResponseText = '';
1812
+ this.promptController?.setStreaming(true);
1813
+ this.promptController?.setStatusMessage('Analyzing request…');
1814
+ const renderer = this.promptController?.getRenderer();
1815
+ let episodeSuccess = false;
1816
+ const toolsUsed = [];
1817
+ const filesModified = [];
1818
+ // Tail of this turn's tool outputs (where TS/test/build errors land), so the
1819
+ // failure registry + governor see real error text, not just the narration.
1820
+ let turnToolOutput = '';
1821
+ // Reviewer findings from THIS turn (set by the adversarial.findings event),
1822
+ // used in the finally to drive a bounded auto-correction.
1823
+ let turnAdversarialFindings = null;
1824
+ // Track reasoning content for fallback when response is empty
1825
+ let reasoningBuffer = '';
1826
+ // Track reasoning-only time to prevent models from reasoning forever without action
1827
+ let reasoningOnlyStartTime = null;
1828
+ let reasoningTimedOut = false;
1829
+ let stepTimedOut = false;
1830
+ let hitlDepth = 0;
1831
+ // Track total prompt processing time to prevent infinite loops
1832
+ const promptStartTime = Date.now();
1833
+ const TOTAL_PROMPT_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24 hours max for entire prompt without meaningful content
1834
+ let hasReceivedMeaningfulContent = false;
1835
+ // Track response content separately - tool calls don't count for reasoning timeout
1836
+ let hasReceivedResponseContent = false;
1837
+ try {
1838
+ // Use timeout-wrapped iterator to prevent hanging on slow/stuck models
1839
+ for await (const eventOrTimeout of iterateWithTimeout(this.controller.send(sanitizedPrompt), PROMPT_STEP_TIMEOUT_MS)) {
1840
+ // Check for timeout marker
1841
+ if (eventOrTimeout && typeof eventOrTimeout === 'object' && '__timeout' in eventOrTimeout) {
1842
+ if (hitlDepth > 0) {
1843
+ this.promptController?.setStatusMessage('Waiting for human decision…');
1844
+ continue;
1845
+ }
1846
+ stepTimedOut = true;
1847
+ this.promptController?.setStatusMessage(`Step timeout (${PROMPT_STEP_TIMEOUT_MS / 1000}s) — completing response`);
1848
+ // Cancel the controller so the underlying agent stops generating
1849
+ // events that would never be consumed. Without this the spinner
1850
+ // can keep ticking against a "ghost" run after the for-await
1851
+ // loop exits, and any in-flight tool keeps doing work the user
1852
+ // can't see or stop.
1853
+ try {
1854
+ this.controller.cancel('step timeout');
1855
+ }
1856
+ catch { /* best-effort */ }
1857
+ break;
1858
+ }
1859
+ // Check total elapsed time - bail out if too long without meaningful content
1860
+ const totalElapsed = Date.now() - promptStartTime;
1861
+ if (!hasReceivedMeaningfulContent && totalElapsed > TOTAL_PROMPT_TIMEOUT_MS) {
1862
+ if (renderer) {
1863
+ renderer.addEvent('response', chalk.yellow(`\nResponse timeout (${Math.round(totalElapsed / 1000)}s) — completing\n`));
1864
+ }
1865
+ reasoningTimedOut = true;
1866
+ try {
1867
+ this.controller.cancel('response timeout');
1868
+ }
1869
+ catch { /* best-effort */ }
1870
+ break;
1871
+ }
1872
+ const event = eventOrTimeout;
1873
+ if (this.shouldExit) {
1874
+ break;
1875
+ }
1876
+ switch (event.type) {
1877
+ case 'message.start':
1878
+ // AI has started processing - update status to show activity
1879
+ this.currentResponseBuffer = '';
1880
+ this.finalResponseText = '';
1881
+ reasoningBuffer = '';
1882
+ reasoningOnlyStartTime = null; // Reset on new message
1883
+ this.promptController?.setStatusMessage('Thinking...');
1884
+ break;
1885
+ case 'message.delta':
1886
+ // Stream content as it arrives
1887
+ this.currentResponseBuffer += event.content ?? '';
1888
+ this.finalResponseText += event.content ?? '';
1889
+ if (renderer) {
1890
+ renderer.addEvent('stream', event.content);
1891
+ }
1892
+ // Reset reasoning timer only when we get actual non-empty content
1893
+ if (event.content && event.content.trim()) {
1894
+ reasoningOnlyStartTime = null;
1895
+ hasReceivedMeaningfulContent = true;
1896
+ hasReceivedResponseContent = true; // Track actual response content
1897
+ }
1898
+ break;
1899
+ case 'reasoning':
1900
+ // Accumulate reasoning for potential fallback synthesis
1901
+ reasoningBuffer += event.content ?? '';
1902
+ // Update status to show reasoning is actively streaming
1903
+ this.promptController?.setActivityMessage('Thinking');
1904
+ // Start the reasoning timer on first reasoning event
1905
+ if (!reasoningOnlyStartTime) {
1906
+ reasoningOnlyStartTime = Date.now();
1907
+ }
1908
+ // Display useful reasoning as 'thought' events BEFORE the response
1909
+ // The renderer's curateReasoningContent and shouldRenderThought will filter
1910
+ // to show only actionable/structured thoughts
1911
+ if (renderer && event.content?.trim()) {
1912
+ renderer.addEvent('thought', event.content);
1913
+ }
1914
+ break;
1915
+ case 'message.complete':
1916
+ // Response complete - clear the thinking indicator
1917
+ this.promptController?.setStatusMessage(null);
1918
+ // Response complete - ensure final output includes required "Next steps"
1919
+ if (renderer) {
1920
+ // Use the appended field from ensureNextSteps to avoid re-rendering the entire response
1921
+ const base = (event.content ?? '').trimEnd();
1922
+ let sourceText = base || this.currentResponseBuffer;
1923
+ // If content came via message.complete but NOT via deltas, render it now as a proper response
1924
+ // This handles models that don't stream deltas (e.g., deepseek-v4-pro)
1925
+ // IMPORTANT: Do NOT re-emit content that was already streamed via 'message.delta' events
1926
+ // to prevent duplicate display of the same response
1927
+ if (base && !this.currentResponseBuffer.trim()) {
1928
+ renderer.addEvent('response', base);
1929
+ }
1930
+ // Note: We intentionally DO NOT re-emit currentResponseBuffer as a 'response' event
1931
+ // because it was already displayed via 'stream' events during message.delta handling
1932
+ // Fallback: If response is empty but we have reasoning, synthesize a response
1933
+ if (!sourceText.trim() && reasoningBuffer.trim()) {
1934
+ // Extract key conclusions from reasoning for display
1935
+ const synthesized = this.synthesizeFromReasoning(reasoningBuffer);
1936
+ if (synthesized) {
1937
+ renderer.addEvent('response', synthesized);
1938
+ sourceText = synthesized;
1939
+ }
1940
+ }
1941
+ episodeSuccess = true; // Mark episode as successful only after we have content
1942
+ // Only add "Next steps" if tools were actually used (real work done)
1943
+ // This prevents showing "Next steps" after reasoning-only responses
1944
+ if (toolsUsed.length > 0) {
1945
+ const { appended } = ensureNextSteps(sourceText);
1946
+ // Only stream the newly appended content (e.g., "Next steps:")
1947
+ // The main response was already added as a response event above
1948
+ if (appended && appended.trim()) {
1949
+ renderer.addEvent('response', appended);
1950
+ }
1951
+ }
1952
+ renderer.addEvent('response', '\n');
1953
+ // Capture the authoritative final text BEFORE the buffer is cleared
1954
+ // (the finally's auto-continue reads run after this clear).
1955
+ this.finalResponseText = sourceText || this.finalResponseText;
1956
+ }
1957
+ this.currentResponseBuffer = '';
1958
+ break;
1959
+ case 'tool.start': {
1960
+ const toolName = event.toolName;
1961
+ const args = event.parameters;
1962
+ if (isHitlToolName(toolName)) {
1963
+ hitlDepth += 1;
1964
+ }
1965
+ // Reset reasoning timer when tools are being called (model is taking action)
1966
+ reasoningOnlyStartTime = null;
1967
+ hasReceivedMeaningfulContent = true;
1968
+ if (!toolsUsed.includes(toolName)) {
1969
+ toolsUsed.push(toolName);
1970
+ }
1971
+ const filePath = (args?.['file_path'] ?? args?.['path']);
1972
+ if (filePath && /edit|write|create|update/i.test(toolName)) {
1973
+ if (!filesModified.includes(filePath)) {
1974
+ filesModified.push(filePath);
1975
+ }
1976
+ }
1977
+ // Claude-Code action line: `⏺ ToolName(primaryArg)`. The dim
1978
+ // present-tense label drives the working spinner above the prompt.
1979
+ // parallel_agents is suppressed here — its per-sub-agent Task notes
1980
+ // (subagent.start/complete) are the visible surface instead of a
1981
+ // raw `parallel_agents({"tasks":…})` JSON dump.
1982
+ if (renderer && toolName !== 'parallel_agents') {
1983
+ renderer.addEvent('tool', formatToolCall(toolName, args, this.workingDir));
1984
+ }
1985
+ this.promptController?.setStatusMessage(toolActivityLabel(toolName, args, this.workingDir));
1986
+ break;
1987
+ }
1988
+ case 'tool.complete': {
1989
+ if (isHitlToolName(event.toolName)) {
1990
+ hitlDepth = Math.max(0, hitlDepth - 1);
1991
+ }
1992
+ // Keep the tail of tool output for the failure registry / governor
1993
+ // (errors land here, not in the assistant narration).
1994
+ if (typeof event.result === 'string' && event.result) {
1995
+ turnToolOutput = (turnToolOutput + '\n' + event.result).slice(-16000);
1996
+ }
1997
+ // Clear the activity label; the agent is thinking again.
1998
+ this.promptController?.setStatusMessage('Thinking…');
1999
+ // Reset reasoning timer after tool completes
2000
+ reasoningOnlyStartTime = null;
2001
+ // Render the result as a dim ` ⎿ …` block (summarised, never a
2002
+ // raw multi-KB dump). Pre-formatted ⏺ blocks (editTools) pass
2003
+ // through with just their duplicate header stripped.
2004
+ if (event.result && typeof event.result === 'string' && event.result.trim() && renderer) {
2005
+ const params = event.parameters;
2006
+ const summary = formatToolResult(event.toolName, event.result, params);
2007
+ renderer.addEvent('tool-result', summary);
2008
+ // Remember the full result so Ctrl+O can expand it — but only
2009
+ // when the summary actually truncated (the `(ctrl+o to expand)`
2010
+ // marker promises the affordance; without truncation there's
2011
+ // nothing to expand). Keeps that promise honest.
2012
+ this.lastExpandableResult = summary.includes('(ctrl+o to expand)')
2013
+ ? { name: event.toolName, result: event.result, params }
2014
+ : null;
2015
+ }
2016
+ break;
2017
+ }
2018
+ case 'tool.error':
2019
+ if (isHitlToolName(event.toolName)) {
2020
+ hitlDepth = Math.max(0, hitlDepth - 1);
2021
+ }
2022
+ this.promptController?.setStatusMessage('Thinking…');
2023
+ if (renderer) {
2024
+ // Red ` ⎿ Error: …` line, mirroring a failed tool result.
2025
+ renderer.addEvent('error', formatToolError(event.error));
2026
+ }
2027
+ break;
2028
+ case 'error':
2029
+ if (renderer) {
2030
+ renderer.addEvent('error', event.error);
2031
+ }
2032
+ break;
2033
+ case 'usage': {
2034
+ // Meter cumulative DeepSeek consumption for /usage + the portal.
2035
+ recordDeepSeekUsage(event.inputTokens, event.outputTokens);
2036
+ // inputTokens = exactly what occupies the context window this turn.
2037
+ // The real model window (not a hardcoded guess) is the denominator
2038
+ // so "% context left" reflects the actual model.
2039
+ const contextTokens = event.inputTokens ?? event.totalTokens ?? null;
2040
+ if (typeof contextTokens === 'number' && contextTokens > 0) {
2041
+ this.lastInputTokens = contextTokens;
2042
+ }
2043
+ const windowTokens = getModelContextInfo(this.profileConfig.model).contextWindow;
2044
+ this.promptController?.setMetaStatus({
2045
+ tokensUsed: contextTokens,
2046
+ tokenLimit: windowTokens,
2047
+ });
2048
+ break;
2049
+ }
2050
+ case 'subagent.start':
2051
+ // A parallel sub-agent spawned — show it like Claude Code's Task.
2052
+ renderer?.addEvent('tool', formatSubAgentStart(event.description));
2053
+ this.promptController?.setStatusMessage(`Running sub-agent: ${event.description}`);
2054
+ break;
2055
+ case 'subagent.complete':
2056
+ renderer?.addEvent('system', chalk.dim(formatSubAgentComplete({
2057
+ description: event.description,
2058
+ success: event.success,
2059
+ elapsedMs: event.elapsedMs,
2060
+ })));
2061
+ break;
2062
+ case 'adversarial.findings':
2063
+ // The reviewer refuted this turn's draft — remember it so the
2064
+ // auto-continue loop can run a bounded re-fix (handled in finally).
2065
+ turnAdversarialFindings = event.findings;
2066
+ break;
2067
+ case 'context.compacted': {
2068
+ // The conversation was auto-compacted to stay within the window —
2069
+ // surface it as a dim note (Claude Code parity) instead of silently.
2070
+ renderer?.addEvent('system', chalk.dim(formatCompactionNote({
2071
+ removed: event.removed,
2072
+ freedTokens: event.freedTokens,
2073
+ summarized: event.summarized,
2074
+ percentage: event.percentage,
2075
+ })));
2076
+ break;
2077
+ }
2078
+ case 'provider.fallback': {
2079
+ // Display fallback notification
2080
+ if (renderer) {
2081
+ const fallbackMsg = chalk.yellow('⚠ ') +
2082
+ chalk.dim(`${event.fromProvider}/${event.fromModel} failed: `) +
2083
+ chalk.hex('#EF4444')(event.reason) +
2084
+ chalk.dim(' → switching to ') +
2085
+ chalk.hex('#34D399')(`${event.toProvider}/${event.toModel}`);
2086
+ renderer.addEvent('banner', fallbackMsg);
2087
+ }
2088
+ // Update the model context to reflect the new provider/model
2089
+ this.profileConfig = {
2090
+ ...this.profileConfig,
2091
+ provider: event.toProvider,
2092
+ model: event.toModel,
2093
+ };
2094
+ this.promptController?.setModelContext({
2095
+ model: event.toModel,
2096
+ provider: event.toProvider,
2097
+ });
2098
+ break;
2099
+ }
2100
+ case 'edit.explanation':
2101
+ // Show explanation for edits made
2102
+ if (event.content && renderer) {
2103
+ const filesInfo = event.files?.length ? ` (${event.files.join(', ')})` : '';
2104
+ renderer.addEvent('response', `${event.content}${filesInfo}`);
2105
+ }
2106
+ break;
2107
+ }
2108
+ // Check reasoning timeout on EVERY iteration (not just when reasoning events arrive)
2109
+ // This ensures we bail out even if events are sparse
2110
+ // Use hasReceivedResponseContent (not hasReceivedMeaningfulContent) so timeout
2111
+ // still triggers after tool calls if model just reasons without responding
2112
+ if (reasoningOnlyStartTime && !hasReceivedResponseContent) {
2113
+ const reasoningElapsed = Date.now() - reasoningOnlyStartTime;
2114
+ if (reasoningElapsed > PROMPT_REASONING_TIMEOUT_MS) {
2115
+ if (renderer) {
2116
+ renderer.addEvent('response', chalk.yellow(`\nReasoning timeout (${Math.round(reasoningElapsed / 1000)}s)\n`));
2117
+ }
2118
+ reasoningTimedOut = true;
2119
+ }
2120
+ }
2121
+ // Check if reasoning timeout was triggered - break out of event loop
2122
+ if (reasoningTimedOut) {
2123
+ // Cancel the controller too; otherwise the for-await drain
2124
+ // exits but the agent keeps producing events and side-effects
2125
+ // for the next 30+ seconds with no UI to consume them.
2126
+ try {
2127
+ this.controller.cancel('reasoning timeout');
2128
+ }
2129
+ catch { /* best-effort */ }
2130
+ break;
2131
+ }
2132
+ }
2133
+ // After loop: synthesize from reasoning if no response was generated or timed out
2134
+ // This handles models like deepseek-v4-pro that output thinking but empty response
2135
+ // Also handles step timeouts where the model was stuck
2136
+ // IMPORTANT: Don't add "Next steps" when only reasoning occurred - only after real work
2137
+ if ((!episodeSuccess || reasoningTimedOut || stepTimedOut) && reasoningBuffer.trim() && !this.currentResponseBuffer.trim()) {
2138
+ const synthesized = this.synthesizeFromReasoning(reasoningBuffer);
2139
+ if (synthesized && renderer) {
2140
+ renderer.addEvent('stream', '\n' + synthesized);
2141
+ // Only add "Next steps" if tools were actually used (real work done)
2142
+ if (toolsUsed.length > 0) {
2143
+ const { appended } = ensureNextSteps(synthesized);
2144
+ if (appended?.trim()) {
2145
+ renderer.addEvent('stream', appended);
2146
+ }
2147
+ }
2148
+ renderer.addEvent('response', '\n');
2149
+ episodeSuccess = true;
2150
+ }
2151
+ }
2152
+ }
2153
+ catch (error) {
2154
+ const message = error instanceof Error ? error.message : String(error);
2155
+ if (renderer) {
2156
+ renderer.addEvent('error', message);
2157
+ }
2158
+ // Fallback: If we have reasoning content but no response was generated, synthesize one
2159
+ if (!episodeSuccess && reasoningBuffer.trim() && !this.currentResponseBuffer.trim()) {
2160
+ const synthesized = this.synthesizeFromReasoning(reasoningBuffer);
2161
+ if (synthesized && renderer) {
2162
+ renderer.addEvent('stream', '\n' + synthesized);
2163
+ renderer.addEvent('response', '\n');
2164
+ episodeSuccess = true; // Mark as partial success
2165
+ }
2166
+ }
2167
+ }
2168
+ finally {
2169
+ // Exit critical section - allow termination again
2170
+ exitCriticalSection();
2171
+ // Final fallback: If stream ended without message.complete but we have reasoning
2172
+ if (!episodeSuccess && reasoningBuffer.trim() && !this.currentResponseBuffer.trim()) {
2173
+ const synthesized = this.synthesizeFromReasoning(reasoningBuffer);
2174
+ if (synthesized && renderer) {
2175
+ renderer.addEvent('stream', '\n' + synthesized);
2176
+ // Only add "Next steps" if tools were actually used (real work done)
2177
+ if (toolsUsed.length > 0) {
2178
+ const { appended } = ensureNextSteps(synthesized);
2179
+ if (appended?.trim()) {
2180
+ renderer.addEvent('stream', appended);
2181
+ }
2182
+ }
2183
+ renderer.addEvent('response', '\n');
2184
+ episodeSuccess = true;
2185
+ }
2186
+ }
2187
+ // Detect a model safety refusal in the just-finished turn. When the
2188
+ // model declines the request, the request is *done* — auto-continue
2189
+ // would just resubmit "continue" and start a new spinner cycle, which
2190
+ // is what produced the stuck "Thinking… (4m N s)" timer the user saw.
2191
+ const refusedTurn = isSafetyRefusal(this.finalResponseText);
2192
+ this.isProcessing = false;
2193
+ this.promptController?.setStreaming(false);
2194
+ this.promptController?.setStatusMessage(null);
2195
+ // Belt-and-suspenders: explicitly clear the activity message so the
2196
+ // "Thinking… (esc to interrupt · Ns)" line doesn't linger after the
2197
+ // final reply if setMode→stopSpinnerAnimation races with another
2198
+ // renderPrompt tick.
2199
+ this.promptController?.setActivityMessage(null);
2200
+ // Force an idle re-render so the spinner area is repainted without
2201
+ // the streaming activity line. setStreaming(false) → setMode('idle')
2202
+ // already calls renderPrompt(), but a coalesced spinner tick that
2203
+ // races with the transition can leave the last "Thinking… (Ns)"
2204
+ // frame on screen until the next event. forceRender squashes it.
2205
+ this.promptController?.forceRender();
2206
+ // Clear any transient follow-up queue UI when we return to idle.
2207
+ const r = this.promptController?.getRenderer();
2208
+ r?.setFollowUpQueueMode(false);
2209
+ r?.setQueuedPrompts([]);
2210
+ // Note: pendingPrompts may still have items if a drain just started
2211
+ // a new processPrompt; the new run will manage the list.
2212
+ // Snapshot this turn's full output (tool results + narration) BEFORE the
2213
+ // buffer is cleared — the auto-continue governor + failure registry need
2214
+ // the real error text, which the reset below would otherwise wipe.
2215
+ const combinedTurnOutput = (turnToolOutput + '\n' + this.finalResponseText).slice(-16000);
2216
+ this.currentResponseBuffer = '';
2217
+ // Autosave the conversation so /resume has something to restore. Each
2218
+ // turn updates the same snapshot in place (keyed by this.sessionId).
2219
+ this.persistSessionSnapshot();
2220
+ // Process any queued prompts (late safety net; primary drain is now per-turn
2221
+ // after each assistant response for "ASAP before the running prompt finishes").
2222
+ if (this.pendingPrompts.length > 0 && !this.shouldExit) {
2223
+ const next = this.pendingPrompts.shift();
2224
+ if (next) {
2225
+ const r = this.promptController?.getRenderer();
2226
+ r?.setFollowUpQueueMode(false);
2227
+ r?.addUserHistoryItem(next);
2228
+ r?.setQueuedPrompts(this.pendingPrompts.slice());
2229
+ await this.processPrompt(next);
2230
+ }
2231
+ }
2232
+ else if (refusedTurn) {
2233
+ // Refusal terminates the turn. Don't re-prompt the model — the
2234
+ // user's request is finished from the agent's side. Clear the
2235
+ // stored "original prompt" so a stray Alt+G later doesn't pick
2236
+ // up where this turn left off.
2237
+ this.originalPromptForAutoContinue = null;
2238
+ }
2239
+ else if (!this.shouldExit && !this.userInterruptedRun) {
2240
+ // Auto mode: keep running until user's prompt is fully completed.
2241
+ // Skipped after a Ctrl+C interrupt so we don't immediately resume
2242
+ // the work the user just cancelled.
2243
+ const autoMode = this.promptController?.getAutoMode() ?? 'off';
2244
+ if (autoMode !== 'off') {
2245
+ // Check if original user prompt is fully completed
2246
+ const detector = getTaskCompletionDetector();
2247
+ const analysis = detector.analyzeCompletion(this.finalResponseText, toolsUsed);
2248
+ // Record this turn with the governor (bounds the loop + detects a
2249
+ // stall: the same tools/files/failure repeating with no new progress)
2250
+ // and the failure registry (catches the same error recurring across
2251
+ // NON-consecutive turns — a thrash the stall check would miss).
2252
+ this.autoGovernor.recordTurn({
2253
+ toolsUsed,
2254
+ filesModified,
2255
+ failingSignal: detectFailingTestOrBuild(combinedTurnOutput),
2256
+ });
2257
+ this.failureRegistry.trackTurn(combinedTurnOutput);
2258
+ const gov = this.autoGovernor.check();
2259
+ const failureNudge = this.failureRegistry.nudge();
2260
+ const todos = getCurrentTodos();
2261
+ const pending = pendingTodos(todos);
2262
+ if (gov.stop) {
2263
+ // Yield to the user WITH state instead of thrashing forever.
2264
+ const note = gov.reason === 'limit'
2265
+ ? `Paused after ${gov.turn} auto-continue turns (turn limit).${pending.length ? ` ${pending.length} task${pending.length === 1 ? '' : 's'} still pending` : ''} — say "continue" to keep going.`
2266
+ : `Paused: no new progress over the last few turns (same actions repeating).${pending.length ? ` ${pending.length} task${pending.length === 1 ? '' : 's'} pending` : ''} — tell me how to proceed.`;
2267
+ this.promptController?.getRenderer()?.addEvent('system', chalk.dim(note));
2268
+ this.promptController?.setStatusMessage(null);
2269
+ this.originalPromptForAutoContinue = null;
2270
+ }
2271
+ else if (turnAdversarialFindings && this.adversarialCorrectionCount < MAX_ADVERSARIAL_CORRECTIONS) {
2272
+ // The reviewer refuted this turn's draft — re-run the FULL tool loop
2273
+ // to actually fix the findings (not just show the caveat), bounded
2274
+ // by the governor + this per-request cap.
2275
+ this.adversarialCorrectionCount += 1;
2276
+ this.promptController?.setStatusMessage('Addressing reviewer findings…');
2277
+ await new Promise(resolve => setTimeout(resolve, 300));
2278
+ await this.processPrompt(buildAdversarialCorrectionPrompt(turnAdversarialFindings));
2279
+ }
2280
+ else if (!analysis.isComplete || pending.length > 0) {
2281
+ // Continue — but only stop when the LIVE PLAN is also clear: pending
2282
+ // todos force a continue even if the response sounded "done".
2283
+ this.promptController?.setStatusMessage('Continuing...');
2284
+ await new Promise(resolve => setTimeout(resolve, 500));
2285
+ // Prefer the plan's next task; fall back to the response heuristic.
2286
+ const base = nextTodoPrompt(todos)
2287
+ ?? this.generateAutoContinuePrompt(this.originalPromptForAutoContinue || '', combinedTurnOutput, toolsUsed)
2288
+ ?? 'continue';
2289
+ // When a failure keeps recurring, lead with the change-approach nudge.
2290
+ // Keep an IMPORTANT: prefix so this counts as an auto-continue (not a
2291
+ // fresh user prompt, which would reset the governor).
2292
+ const autoPrompt = failureNudge
2293
+ ? `IMPORTANT: ${failureNudge}\n\n${base.replace(/^IMPORTANT:\s*/, '')}`
2294
+ : base;
2295
+ await this.processPrompt(autoPrompt);
2296
+ }
2297
+ else {
2298
+ this.promptController?.setStatusMessage('Task complete');
2299
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
2300
+ }
2301
+ }
2302
+ }
2303
+ }
2304
+ }
2305
+ generateAutoContinuePrompt(originalPrompt, response, toolsUsed) {
2306
+ // Highest-priority signal: a test or build is currently failing
2307
+ // in the visible output. Override every other heuristic and force
2308
+ // a sharp, focused next-action prompt — the agent must drill into
2309
+ // the FIRST failure rather than declaring victory.
2310
+ const failingSignal = detectFailingTestOrBuild(response);
2311
+ if (failingSignal) {
2312
+ const noDocsInstruction = `IMPORTANT: Do NOT create markdown files, documentation, summaries, or reports.`;
2313
+ return `${noDocsInstruction} The output above shows a failing test/build (${failingSignal}). Read the FIRST failure carefully, identify the root cause, edit exactly the file(s) needed, then re-run the same test/build command to confirm. Do not stop until that command exits cleanly.`;
2314
+ }
2315
+ // Only auto-continue for certain types of work
2316
+ const hasFileOperations = toolsUsed.some(t => ['Read', 'Write', 'Edit', 'Search', 'Grep'].includes(t));
2317
+ const hasBashOperations = toolsUsed.includes('Bash');
2318
+ if (!hasFileOperations && !hasBashOperations) {
2319
+ return null; // No meaningful work to continue
2320
+ }
2321
+ // Analyze response to determine what to do next
2322
+ const lowercaseResponse = response.toLowerCase();
2323
+ // Check for common patterns that indicate more work is needed
2324
+ if (lowercaseResponse.includes('next steps') ||
2325
+ lowercaseResponse.includes('further') ||
2326
+ lowercaseResponse.includes('additional') ||
2327
+ lowercaseResponse.includes('implement') ||
2328
+ lowercaseResponse.includes('complete') ||
2329
+ lowercaseResponse.includes('finish')) {
2330
+ // Core instruction to prevent documentation spam
2331
+ const noDocsInstruction = `IMPORTANT: Do NOT create markdown files, documentation, summaries, or reports. Focus only on the actual code/implementation work. Perform the next concrete action in the codebase.`;
2332
+ // Generate a follow-up prompt based on the original task
2333
+ if (originalPrompt.includes('fix') || originalPrompt.includes('bug')) {
2334
+ return `${noDocsInstruction} Continue fixing - edit the next file that needs changes.`;
2335
+ }
2336
+ else if (originalPrompt.includes('implement') || originalPrompt.includes('add')) {
2337
+ return `${noDocsInstruction} Continue implementing - write or edit the next piece of code.`;
2338
+ }
2339
+ else if (originalPrompt.includes('refactor') || originalPrompt.includes('clean')) {
2340
+ return `${noDocsInstruction} Continue refactoring - apply changes to the next file.`;
2341
+ }
2342
+ else if (originalPrompt.includes('test')) {
2343
+ return `${noDocsInstruction} Continue with tests - run or fix the next test.`;
2344
+ }
2345
+ else if (originalPrompt.includes('build') || originalPrompt.includes('deploy') || originalPrompt.includes('publish')) {
2346
+ return `${noDocsInstruction} Continue the build/deploy process - execute the next command.`;
2347
+ }
2348
+ else {
2349
+ return `${noDocsInstruction} Continue with the original task "${originalPrompt.slice(0, 100)}..." - perform the next action.`;
2350
+ }
2351
+ }
2352
+ return null;
2353
+ }
2354
+ handleInterrupt() {
2355
+ if (!this.isProcessing) {
2356
+ return;
2357
+ }
2358
+ const renderer = this.promptController?.getRenderer();
2359
+ if (renderer) {
2360
+ renderer.addEvent('banner', chalk.yellow('Interrupted'));
2361
+ }
2362
+ // Actually cancel the in-flight controller run. Without this the
2363
+ // for-await loop in processPrompt keeps consuming events, the spinner
2364
+ // stays up, and the agent grinds through the rest of its tool loop
2365
+ // while the user sees only a "Interrupted" banner. cancel() is a no-op
2366
+ // when there's no active sink, so this is safe to call unconditionally.
2367
+ try {
2368
+ this.controller.cancel('user interrupt via Ctrl+C');
2369
+ }
2370
+ catch {
2371
+ // Best-effort; if the controller is already torn down the next
2372
+ // Ctrl+C will fall through to authorizedShutdown.
2373
+ }
2374
+ // Suppress the auto-continue re-launch in processPrompt's finally
2375
+ // block. Otherwise the agent immediately starts a fresh "continue"
2376
+ // cycle 500ms later and the user has to keep mashing Ctrl+C to keep
2377
+ // up. Cleared when the user submits a new prompt.
2378
+ this.userInterruptedRun = true;
2379
+ }
2380
+ handleAutoContinueToggle() {
2381
+ const autoMode = this.promptController?.getAutoMode() ?? 'off';
2382
+ this.promptController?.setStatusMessage(`Auto: ${autoMode}`);
2383
+ setTimeout(() => this.promptController?.setStatusMessage(null), 1500);
2384
+ // Reset task completion detector when entering any auto mode
2385
+ if (autoMode !== 'off') {
2386
+ const detector = getTaskCompletionDetector();
2387
+ detector.reset();
2388
+ // Clear any stored original prompt
2389
+ this.originalPromptForAutoContinue = null;
2390
+ }
2391
+ }
2392
+ handleHITLToggle() {
2393
+ const mode = this.promptController?.getModeToggleState().hitlMode ?? 'off';
2394
+ getHITL().updateConfig({ autoPause: mode === 'on' });
2395
+ this.promptController?.setStatusMessage(`HITL: ${mode}`);
2396
+ setTimeout(() => this.promptController?.setStatusMessage(null), 1500);
2397
+ }
2398
+ /**
2399
+ * Shift+Tab cycled the permission mode. The hint line under the input box
2400
+ * already shows the active mode; this surfaces a brief one-line note in
2401
+ * the chat so the change is unmistakable, matching how Claude Code echoes
2402
+ * a mode switch.
2403
+ */
2404
+ handlePermissionModeChange(mode) {
2405
+ const note = mode === 'plan'
2406
+ ? 'plan mode — read-only; I won’t edit files or run commands until you approve a plan'
2407
+ : mode === 'acceptEdits'
2408
+ ? 'accept edits on — file edits apply without the adversarial pre-flight'
2409
+ : 'default mode';
2410
+ this.promptController?.setStatusMessage(note);
2411
+ setTimeout(() => this.promptController?.setStatusMessage(null), 2500);
2412
+ }
2413
+ /**
2414
+ * Ctrl+O — expand the last truncated tool result. The `(ctrl+o to expand)`
2415
+ * marker promises this; we honor it by re-emitting the SAME tool result with
2416
+ * no line cap (a huge maxLines), appended below as a dim block. If nothing is
2417
+ * pending (the last result fit on screen), a brief status note says so rather
2418
+ * than silently doing nothing.
2419
+ */
2420
+ handleExpandToolResult() {
2421
+ const last = this.lastExpandableResult;
2422
+ const renderer = this.promptController?.getRenderer();
2423
+ if (!last || !renderer) {
2424
+ this.promptController?.setStatusMessage('Nothing to expand');
2425
+ setTimeout(() => this.promptController?.setStatusMessage(null), 1500);
2426
+ return;
2427
+ }
2428
+ // Re-render the full result (no truncation). One expand per result.
2429
+ this.lastExpandableResult = null;
2430
+ renderer.addEvent('tool-result', formatToolResult(last.name, last.result, last.params, { maxLines: 100000 }));
2431
+ }
2432
+ handleCtrlC(info) {
2433
+ const now = Date.now();
2434
+ // Reset count if more than 2 seconds since last Ctrl+C
2435
+ if (now - this.lastCtrlCTime > 2000) {
2436
+ this.ctrlCCount = 0;
2437
+ }
2438
+ this.lastCtrlCTime = now;
2439
+ this.ctrlCCount++;
2440
+ if (info.hadBuffer) {
2441
+ // Clear buffer, reset count
2442
+ this.ctrlCCount = 0;
2443
+ return;
2444
+ }
2445
+ // Always allow double Ctrl+C to exit, even while processing
2446
+ if (this.ctrlCCount >= 2) {
2447
+ // Use authorized shutdown to bypass anti-termination guard
2448
+ void authorizedShutdown(0);
2449
+ this.shouldExit = true;
2450
+ this.ctrlCCount = 0;
2451
+ return;
2452
+ }
2453
+ if (this.isProcessing) {
2454
+ // Interrupt processing on first Ctrl+C, then allow next Ctrl+C to exit
2455
+ this.handleInterrupt();
2456
+ const renderer = this.promptController?.getRenderer();
2457
+ if (renderer) {
2458
+ renderer.addEvent('banner', chalk.dim('Press Ctrl+C again to exit'));
2459
+ }
2460
+ return;
2461
+ }
2462
+ // First Ctrl+C when idle: show hint
2463
+ const renderer = this.promptController?.getRenderer();
2464
+ if (renderer) {
2465
+ renderer.addEvent('banner', chalk.dim('Press Ctrl+C again to exit'));
2466
+ }
2467
+ }
2468
+ handleExit() {
2469
+ this.shouldExit = true;
2470
+ this.cleanupSudoPasswordHandler();
2471
+ this.promptController?.stop();
2472
+ void authorizedShutdown(0);
2473
+ }
2474
+ waitForExit() {
2475
+ return new Promise((resolve) => {
2476
+ const check = () => {
2477
+ if (this.shouldExit) {
2478
+ resolve();
2479
+ }
2480
+ else {
2481
+ setTimeout(check, 100);
2482
+ }
2483
+ };
2484
+ check();
2485
+ });
2486
+ }
2487
+ }
2488
+ // The --profile / -p flag was removed; the only call site passes nothing.
2489
+ // We retain the function as a single source of truth for the hardcoded
2490
+ // profile name that downstream config (agent prompt, model, rulebook)
2491
+ // keys off of.
2492
+ function resolveProfile() {
2493
+ return 'trenchwork-code';
2494
+ }
2495
+ //# sourceMappingURL=interactiveShell.js.map