carthagent 0.0.0-stage → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (379) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -2
  3. package/bin/ctg.mjs +736 -0
  4. package/docs/budgets.md +72 -0
  5. package/docs/compaction.md +34 -0
  6. package/docs/context-efficiency-plan.md +24 -0
  7. package/docs/context-efficiency-review.md +27 -0
  8. package/docs/context-efficiency.md +49 -0
  9. package/docs/deepseek-jev-benchmark-plan.md +70 -0
  10. package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
  11. package/docs/deepseek-jev-benchmark-restart.md +9 -0
  12. package/docs/deepseek-jev-benchmark-results.md +77 -0
  13. package/docs/deepseek-jev-benchmark-running.md +60 -0
  14. package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
  15. package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
  16. package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
  17. package/docs/deepseek-jev-http400-diagnosis.md +45 -0
  18. package/docs/delivery-review-evidence.md +53 -0
  19. package/docs/delivery-timeout-repair-plan.md +51 -0
  20. package/docs/evaluation-readiness.md +31 -0
  21. package/docs/evaluation.md +70 -0
  22. package/docs/extending.md +354 -0
  23. package/docs/glm-pilot-execution.md +9 -0
  24. package/docs/glm-pilot-plan.md +15 -0
  25. package/docs/glm-pilot-results.md +71 -0
  26. package/docs/graph-context-optimization.md +15 -0
  27. package/docs/jev-repository-pilot-plan.md +25 -0
  28. package/docs/jev-repository-pilot-protocol.md +39 -0
  29. package/docs/jev-repository-pilot-restart.md +7 -0
  30. package/docs/jev-repository-pilot-results.md +79 -0
  31. package/docs/jev.md +32 -0
  32. package/docs/model-registry.md +25 -0
  33. package/docs/native-compatibility.md +117 -0
  34. package/docs/opencode-archive.md +9 -0
  35. package/docs/opencode-migration.md +115 -0
  36. package/docs/opencode-runtime.md +94 -0
  37. package/docs/opencode-workspace.md +31 -0
  38. package/docs/owned-runtime-workflow-registration.md +17 -0
  39. package/docs/plan-advisor-evaluation.md +38 -0
  40. package/docs/planning-startup-fix-plan.md +23 -0
  41. package/docs/planning-startup-fix-review.md +24 -0
  42. package/docs/planning.md +118 -0
  43. package/docs/production-merge-review.md +23 -0
  44. package/docs/quality.md +35 -0
  45. package/docs/repository-benchmark-design.md +111 -0
  46. package/docs/review-assurance.md +9 -0
  47. package/docs/review-experiment-results.md +59 -0
  48. package/docs/tested-planning-roadmap.md +58 -0
  49. package/docs/tested-planning.md +58 -0
  50. package/docs/tui-pickers.md +9 -0
  51. package/docs/webdriver.md +83 -0
  52. package/docs/workflow-modes.md +26 -0
  53. package/docs/workflow.d2 +17 -0
  54. package/docs/workflow.svg +149 -0
  55. package/docs/workspace-coordination.md +9 -0
  56. package/evaluate.mjs +249 -0
  57. package/extensions/codegraph.ts +36 -0
  58. package/extensions/delivery.ts +819 -0
  59. package/extensions/experiential.ts +21 -0
  60. package/extensions/subscriptions.ts +21 -0
  61. package/guidance/profiles/authenticated-web.json +24 -0
  62. package/guidance/profiles/data-visualization.json +25 -0
  63. package/guidance/profiles/database-change.json +24 -0
  64. package/guidance/profiles/external-api.json +24 -0
  65. package/guidance/profiles/transactional-data.json +25 -0
  66. package/guidance/profiles/web-application.json +26 -0
  67. package/lib/acceptance-review.mjs +4 -0
  68. package/lib/browser-bin.mjs +160 -0
  69. package/lib/browser-errors.mjs +54 -0
  70. package/lib/browser.mjs +219 -0
  71. package/lib/budget.mjs +37 -0
  72. package/lib/cdp.mjs +221 -0
  73. package/lib/cloud/client.mjs +275 -0
  74. package/lib/cloud/control-plane.mjs +350 -0
  75. package/lib/cloud/gateway.mjs +380 -0
  76. package/lib/cloud/http.mjs +135 -0
  77. package/lib/cloud/ledger.mjs +215 -0
  78. package/lib/codegraph-query.mjs +60 -0
  79. package/lib/codegraph.mjs +378 -0
  80. package/lib/context-archive.mjs +18 -0
  81. package/lib/delivery-context.mjs +132 -0
  82. package/lib/delivery-guidance.mjs +9 -0
  83. package/lib/delivery-review.mjs +94 -0
  84. package/lib/delivery.mjs +610 -0
  85. package/lib/engine.mjs +137 -0
  86. package/lib/guidance.mjs +71 -0
  87. package/lib/jev-advisor.mjs +47 -0
  88. package/lib/opencode/archive.mjs +122 -0
  89. package/lib/opencode/compatibility.mjs +19 -0
  90. package/lib/opencode/credentials.mjs +205 -0
  91. package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
  92. package/lib/opencode/evaluation.mjs +242 -0
  93. package/lib/opencode/file-lock.mjs +86 -0
  94. package/lib/opencode/headless.mjs +200 -0
  95. package/lib/opencode/review.mjs +213 -0
  96. package/lib/opencode/runtime.mjs +246 -0
  97. package/lib/opencode/service.mjs +249 -0
  98. package/lib/opencode/session-options.mjs +60 -0
  99. package/lib/opencode/workspace.mjs +133 -0
  100. package/lib/openrouter-jev.mjs +37 -0
  101. package/lib/outcome-contract.mjs +21 -0
  102. package/lib/plan-advisor.mjs +38 -0
  103. package/lib/planning-access.mjs +7 -0
  104. package/lib/planning-advice.mjs +28 -0
  105. package/lib/planning-discovery.mjs +14 -0
  106. package/lib/planning-guide.mjs +34 -0
  107. package/lib/planning-view.mjs +57 -0
  108. package/lib/planning.mjs +155 -0
  109. package/lib/probe-diagnostics.mjs +13 -0
  110. package/lib/providers/experiential.mjs +292 -0
  111. package/lib/providers/model-catalog.json +700 -0
  112. package/lib/providers/model-catalog.mjs +7 -0
  113. package/lib/providers/names.mjs +13 -0
  114. package/lib/providers/subscription-models.mjs +483 -0
  115. package/lib/reports.mjs +80 -0
  116. package/lib/review-state.mjs +49 -0
  117. package/lib/review.mjs +114 -0
  118. package/lib/scenarios.mjs +99 -0
  119. package/lib/ship-auth.mjs +44 -0
  120. package/lib/ship-jev.mjs +59 -0
  121. package/lib/ship-telemetry.mjs +183 -0
  122. package/lib/tui/ansi.mjs +255 -0
  123. package/lib/tui/app.mjs +2445 -0
  124. package/lib/tui/auth.mjs +249 -0
  125. package/lib/tui/carthage.mjs +109 -0
  126. package/lib/tui/clipboard.mjs +122 -0
  127. package/lib/tui/d2.mjs +431 -0
  128. package/lib/tui/feed.mjs +467 -0
  129. package/lib/tui/framing.mjs +80 -0
  130. package/lib/tui/glyphs.mjs +172 -0
  131. package/lib/tui/hash-worker.mjs +11 -0
  132. package/lib/tui/mock-engine.mjs +222 -0
  133. package/lib/tui/models.mjs +73 -0
  134. package/lib/tui/picker.mjs +18 -0
  135. package/lib/tui/theme.mjs +663 -0
  136. package/lib/tui/title.mjs +7 -0
  137. package/lib/update-channel.mjs +62 -0
  138. package/lib/update.mjs +230 -0
  139. package/lib/webdriver.mjs +82 -0
  140. package/lib/workflow.mjs +88 -0
  141. package/lib/workspace.mjs +39 -0
  142. package/metadata.json +8 -0
  143. package/package.json +94 -4
  144. package/plugins/carthagent/index.ts +3 -0
  145. package/plugins/carthagent/package.json +28 -0
  146. package/plugins/carthagent/src/archive-rpc.ts +46 -0
  147. package/plugins/carthagent/src/bootstrap.ts +38 -0
  148. package/plugins/carthagent/src/index.ts +62 -0
  149. package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
  150. package/plugins/carthagent/src/rpc.ts +76 -0
  151. package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
  152. package/plugins/carthagent/src/server/budgets.ts +21 -0
  153. package/plugins/carthagent/src/server/codegraph.ts +46 -0
  154. package/plugins/carthagent/src/server/delivery.ts +488 -0
  155. package/plugins/carthagent/src/server/device-auth.ts +91 -0
  156. package/plugins/carthagent/src/server/native-delivery.ts +26 -0
  157. package/plugins/carthagent/src/server/orchestration.ts +56 -0
  158. package/plugins/carthagent/src/server/policy.ts +24 -0
  159. package/plugins/carthagent/src/server/providers.ts +424 -0
  160. package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
  161. package/plugins/carthagent/src/server/session-controls.ts +82 -0
  162. package/plugins/carthagent/src/server/telemetry.ts +92 -0
  163. package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
  164. package/plugins/carthagent/src/session-rpc.ts +31 -0
  165. package/plugins/carthagent/src/testing.ts +85 -0
  166. package/plugins/carthagent/src/workspace-rpc.ts +133 -0
  167. package/plugins/carthagent/test-plugin/index.ts +4 -0
  168. package/plugins/carthagent/test-plugin/package.json +1 -0
  169. package/plugins/carthagent/tsconfig.json +19 -0
  170. package/plugins/carthagent/tui.ts +1 -0
  171. package/prompts/delivery.md +7 -0
  172. package/public/index.html +983 -0
  173. package/scenarios/cli/grade_cli.py +69 -0
  174. package/scenarios/cli/scenario.json +21 -0
  175. package/scenarios/game/baseline/minecraft.py +193 -0
  176. package/scenarios/game/context.md +1 -0
  177. package/scenarios/game/grade_minecraft_clone.py +603 -0
  178. package/scenarios/game/requirements.txt +2 -0
  179. package/scenarios/game/scenario.json +33 -0
  180. package/server.mjs +341 -0
  181. package/skills/game-development/SKILL.md +36 -0
  182. package/skills/game-development/assets/ursina_starter.py +98 -0
  183. package/skills/game-development/scripts/verify_ursina.py +80 -0
  184. package/skills/software-delivery/SKILL.md +50 -0
  185. package/skills/typescript-delivery/SKILL.md +69 -0
  186. package/tests/acceptance-review.test.mjs +13 -0
  187. package/tests/auth.test.mjs +234 -0
  188. package/tests/bounded-summary.test.mjs +68 -0
  189. package/tests/browser-bin.test.mjs +91 -0
  190. package/tests/browser.test.mjs +96 -0
  191. package/tests/budget-engine.test.mjs +110 -0
  192. package/tests/budget.test.mjs +23 -0
  193. package/tests/cli.test.mjs +245 -0
  194. package/tests/clipboard.test.mjs +79 -0
  195. package/tests/cloud-client.test.mjs +129 -0
  196. package/tests/cloud-control-plane.test.mjs +144 -0
  197. package/tests/cloud-gateway.test.mjs +147 -0
  198. package/tests/cloud-http.test.mjs +102 -0
  199. package/tests/cloud-ledger.test.mjs +75 -0
  200. package/tests/codegraph-query.test.mjs +66 -0
  201. package/tests/codegraph.test.mjs +142 -0
  202. package/tests/delivery-context.test.mjs +66 -0
  203. package/tests/delivery-review.test.mjs +139 -0
  204. package/tests/delivery.test.mjs +343 -0
  205. package/tests/engine-api.mjs +24 -0
  206. package/tests/experiential-extension.test.mjs +37 -0
  207. package/tests/experiential.test.mjs +300 -0
  208. package/tests/extension.test.mjs +921 -0
  209. package/tests/fixtures/archive-plugin/index.ts +10 -0
  210. package/tests/guidance.test.mjs +78 -0
  211. package/tests/model-registry.test.mjs +68 -0
  212. package/tests/models.test.mjs +70 -0
  213. package/tests/openai-idempotency.test.mjs +99 -0
  214. package/tests/opencode-archive-rpc.test.mjs +82 -0
  215. package/tests/opencode-delivery.test.mjs +231 -0
  216. package/tests/opencode-providers.test.mjs +414 -0
  217. package/tests/opencode-runtime.test.mjs +455 -0
  218. package/tests/opencode-session-controls.test.mjs +115 -0
  219. package/tests/opencode-session-options.test.mjs +26 -0
  220. package/tests/opencode-workspace.test.mjs +339 -0
  221. package/tests/outcome-contract.test.mjs +17 -0
  222. package/tests/picker.test.mjs +34 -0
  223. package/tests/plan-advisor.test.mjs +88 -0
  224. package/tests/plan-review-loop.test.mjs +53 -0
  225. package/tests/planning-advice.test.mjs +27 -0
  226. package/tests/planning-contract.test.mjs +65 -0
  227. package/tests/planning-startup.test.mjs +38 -0
  228. package/tests/planning-view.test.mjs +33 -0
  229. package/tests/planning.test.mjs +223 -0
  230. package/tests/probe-diagnostics.test.mjs +21 -0
  231. package/tests/pull-request-rpc.test.mjs +366 -0
  232. package/tests/review-state.test.mjs +32 -0
  233. package/tests/review.test.mjs +127 -0
  234. package/tests/server.test.mjs +252 -0
  235. package/tests/ship-jev.test.mjs +77 -0
  236. package/tests/ship-telemetry.test.mjs +333 -0
  237. package/tests/standard-workflow.test.mjs +28 -0
  238. package/tests/subscription-models.test.mjs +416 -0
  239. package/tests/subscriptions-extension.test.mjs +27 -0
  240. package/tests/terminal-title.test.mjs +13 -0
  241. package/tests/test_grade_minecraft_clone.py +199 -0
  242. package/tests/test_starter.py +78 -0
  243. package/tests/theme.test.mjs +175 -0
  244. package/tests/tui-copy-paste.test.mjs +54 -0
  245. package/tests/tui.test.mjs +808 -0
  246. package/tests/update-channels.test.mjs +129 -0
  247. package/tests/update.test.mjs +175 -0
  248. package/tests/webdriver.test.mjs +31 -0
  249. package/tests/workflow.test.mjs +47 -0
  250. package/tests/workspace-rpc.test.mjs +236 -0
  251. package/tests/workspace.test.mjs +24 -0
  252. package/tools/benchmark/advisor.test.mjs +7 -0
  253. package/tools/benchmark/advisor.ts +27 -0
  254. package/tools/benchmark/campaign.mjs +45 -0
  255. package/tools/benchmark/diagnose-request.mjs +40 -0
  256. package/tools/benchmark/eval-common.mjs +32 -0
  257. package/tools/benchmark/eval-ledger.mjs +22 -0
  258. package/tools/benchmark/eval-shop.mjs +26 -0
  259. package/tools/benchmark/eval-voxel.mjs +20 -0
  260. package/tools/benchmark/evaluate.mjs +24 -0
  261. package/tools/benchmark/evaluators.test.mjs +13 -0
  262. package/tools/benchmark/fixtures/shop/server.mjs +18 -0
  263. package/tools/benchmark/fixtures/shop/ui.html +8 -0
  264. package/tools/benchmark/freeze.mjs +30 -0
  265. package/tools/benchmark/gateway.mjs +91 -0
  266. package/tools/benchmark/gateway.test.mjs +38 -0
  267. package/tools/benchmark/generator.mjs +11 -0
  268. package/tools/benchmark/generator.test.mjs +26 -0
  269. package/tools/benchmark/guard.test.mjs +133 -0
  270. package/tools/benchmark/guard.ts +136 -0
  271. package/tools/benchmark/native-plugin/index.ts +24 -0
  272. package/tools/benchmark/planning-startup.test.mjs +47 -0
  273. package/tools/benchmark/preflight-live.mjs +31 -0
  274. package/tools/benchmark/preflight.mjs +22 -0
  275. package/tools/benchmark/replay-diagnostic.mjs +23 -0
  276. package/tools/benchmark/runner.mjs +141 -0
  277. package/tools/benchmark/runner.test.mjs +93 -0
  278. package/tools/benchmark/runtime.mjs +23 -0
  279. package/tools/benchmark/selftest.mjs +47 -0
  280. package/tools/benchmark/smoke-jev.mjs +5 -0
  281. package/tools/benchmark/tasks.mjs +20 -0
  282. package/tools/benchmark/transport.mjs +65 -0
  283. package/tools/benchmark/transport.test.mjs +44 -0
  284. package/tools/benchmark/upstream-error.mjs +18 -0
  285. package/tools/benchmark/upstream-error.test.mjs +10 -0
  286. package/tools/browser-check.mjs +109 -0
  287. package/tools/check-syntax.mjs +27 -0
  288. package/tools/evaluate-plan-advisor.mjs +20 -0
  289. package/tools/native-fixture.mjs +194 -0
  290. package/tools/native-pty-smoke.py +108 -0
  291. package/tools/native-report-plugin/index.ts +50 -0
  292. package/tools/native-report-plugin/package.json +7 -0
  293. package/tools/patch-compaction.mjs +24 -0
  294. package/tools/patch-model-catalog.mjs +12 -0
  295. package/tools/patch-print-shutdown.mjs +39 -0
  296. package/tools/repo-pilot/evaluator.cjs +32 -0
  297. package/tools/repo-pilot/reference.mjs +43 -0
  298. package/tools/repo-pilot/runner.mjs +85 -0
  299. package/tools/repo-pilot/sandbox-entry.mjs +12 -0
  300. package/tools/repo-pilot/tasks.mjs +20 -0
  301. package/tools/repo-pilot/type-evaluator.cjs +6 -0
  302. package/tools/test-native-controls.mjs +84 -0
  303. package/tools/test-native-delivery-host.mjs +307 -0
  304. package/tools/test-native-evaluation.mjs +46 -0
  305. package/tools/test-native-host.mjs +385 -0
  306. package/tools/test-native-outcomes.mjs +73 -0
  307. package/tools/test-native-package.mjs +70 -0
  308. package/tools/test-native-pty.mjs +20 -0
  309. package/tools/test-native-report.mjs +88 -0
  310. package/tools/test-native-swe-host.mjs +132 -0
  311. package/tools/test-native-swe.mjs +122 -0
  312. package/tools/test-picker-pty.py +45 -0
  313. package/tools/test-python.mjs +29 -0
  314. package/tools/test-webdriver.mjs +54 -0
  315. package/tools/verify-release-version.mjs +37 -0
  316. package/tools/webdriver-check.mjs +52 -0
  317. package/vendor/agent/bounded-summary.mjs +88 -0
  318. package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
  319. package/vendor/agent/chunks/anthropic.js +75 -0
  320. package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
  321. package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
  322. package/vendor/agent/chunks/chord-context.js +96 -0
  323. package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
  324. package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
  325. package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
  326. package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
  327. package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
  328. package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
  329. package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
  330. package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
  331. package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
  332. package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
  333. package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
  334. package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
  335. package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
  336. package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
  337. package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
  338. package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
  339. package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
  340. package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
  341. package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
  342. package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
  343. package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
  344. package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
  345. package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
  346. package/vendor/agent/chunks/github-copilot.js +2 -0
  347. package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
  348. package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
  349. package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
  350. package/vendor/agent/chunks/image-resize-worker.js +2 -0
  351. package/vendor/agent/chunks/kimi-coding.js +2 -0
  352. package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
  353. package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
  354. package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
  355. package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
  356. package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
  357. package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
  358. package/vendor/agent/chunks/openai-codex.js +75 -0
  359. package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
  360. package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
  361. package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
  362. package/vendor/agent/chunks/openrouter.js +75 -0
  363. package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
  364. package/vendor/agent/chunks/radius.js +75 -0
  365. package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
  366. package/vendor/agent/chunks/xai.js +2 -0
  367. package/vendor/agent/cli.js +3 -0
  368. package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
  369. package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
  370. package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
  371. package/vendor/agent/index.js +2 -0
  372. package/vendor/agent/modes/interactive/theme/dark.json +91 -0
  373. package/vendor/agent/modes/interactive/theme/light.json +90 -0
  374. package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
  375. package/vendor/agent/package.json +14 -0
  376. package/vendor/agent/rpc-entry.js +3 -0
  377. package/vendor/agent/theme/dark.json +91 -0
  378. package/vendor/agent/theme/light.json +90 -0
  379. package/vendor/agent/theme/theme-schema.json +356 -0
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Event → feed reducer for the carthagent TUI. Pure data: blocks describe what the
3
+ * run terminal shows, derived stats describe the side panel. Rendering turns
4
+ * blocks into styled lines (see renderFeed).
5
+ */
6
+ import { fg, BOLD, DIM, ITALIC, RESET, wrap, truncate, mix } from './ansi.mjs';
7
+ import { unframe } from './framing.mjs';
8
+ import { UNICODE_GLYPHS, spinnerFrame } from './glyphs.mjs';
9
+ import { CARTHAGE_LINES, pickCarthageLine } from './carthage.mjs';
10
+
11
+ /** Unicode tool icons, kept for backwards compatibility with callers that
12
+ * imported the constant. Rendering resolves the active tier via `TOOL_ICON_KEYS`. */
13
+ export const TOOL_ICONS = {
14
+ read: '◈', bash: '▸', powershell: '▸', write: '✎', edit: '✎', grep: '⌕', find: '⌕', ls: '▤',
15
+ delivery_design: '◇', delivery_plan: '◇', delivery_revise: '◇', delivery_review: '⚑', delivery_check: '⚑', delivery_finish: '⚑', delivery_status: '·',
16
+ };
17
+
18
+ const TOOL_ICON_KEYS = {
19
+ read: 'read', bash: 'run', powershell: 'run', write: 'write', edit: 'write',
20
+ grep: 'search', find: 'search', ls: 'list',
21
+ delivery_design: 'plan', delivery_plan: 'plan', delivery_revise: 'plan', delivery_review: 'check', delivery_check: 'check', delivery_finish: 'check', delivery_status: 'status',
22
+ };
23
+
24
+ export function toolIcon(glyphs, name) {
25
+ return glyphs[TOOL_ICON_KEYS[name]] || glyphs.bullet;
26
+ }
27
+
28
+ export function createFeed() {
29
+ return {
30
+ blocks: [], // ordered render units
31
+ toolBlocks: new Map(), // toolCallId -> block
32
+ running: false,
33
+ settled: true,
34
+ turns: 0,
35
+ tokens: 0,
36
+ cost: 0,
37
+ toolCalls: 0,
38
+ currentAssistant: null,
39
+ currentThinking: null,
40
+ completionBlock: null,
41
+ version: 0, // bumped on any mutation (for render caching)
42
+ };
43
+ }
44
+
45
+ export function resetFeed(S) {
46
+ S.blocks = [];
47
+ S.toolBlocks.clear();
48
+ S.running = false;
49
+ S.settled = true;
50
+ S.turns = 0;
51
+ S.tokens = 0;
52
+ S.cost = 0;
53
+ S.toolCalls = 0;
54
+ S.currentAssistant = null;
55
+ S.currentThinking = null;
56
+ S.completionBlock = null;
57
+ S.deliveryStatusText = null;
58
+ S.userBash = null;
59
+ S.version++;
60
+ return S;
61
+ }
62
+
63
+ function push(S, block) {
64
+ block.t = Date.now();
65
+ S.blocks.push(block);
66
+ if (S.blocks.length > 800) S.blocks.splice(0, S.blocks.length - 800);
67
+ S.version++;
68
+ return block;
69
+ }
70
+
71
+ export function textOf(content) {
72
+ if (!Array.isArray(content)) return typeof content === 'string' ? content : '';
73
+ return content.filter(c => c?.type === 'text').map(c => c.text).join('\n');
74
+ }
75
+
76
+ function resultText(result) {
77
+ if (result == null) return '';
78
+ if (typeof result === 'string') return result;
79
+ if (Array.isArray(result.content)) return textOf(result.content);
80
+ try { return JSON.stringify(result); } catch { return String(result); }
81
+ }
82
+
83
+ export function tailLines(s, n) {
84
+ const lines = String(s).replace(/\n+$/, '').split('\n');
85
+ return lines.slice(-n);
86
+ }
87
+
88
+ /** Reduce one JSON-mode session event into the feed model. */
89
+ export function applyEvent(S, ev) {
90
+ const ase = ev.assistantMessageEvent;
91
+ switch (ev.type) {
92
+ case 'agent_start':
93
+ S.running = true; S.settled = false;
94
+ S.completionBlock = null;
95
+ // Visual separator between runs — the local echo path pushes one before
96
+ // the user block, so here we only need it for extension-driven resumes.
97
+ if (S.blocks.length && !['runline', 'user'].includes(S.blocks[S.blocks.length - 1].kind)) {
98
+ push(S, { kind: 'runline' });
99
+ }
100
+ S.version++;
101
+ break;
102
+ case 'agent_settled':
103
+ S.settled = true; S.running = false;
104
+ if (S.currentAssistant) S.currentAssistant.streaming = false;
105
+ if (S.currentThinking) S.currentThinking.streaming = false;
106
+ if (S.runCompletionPending) {
107
+ S.completionBlock = push(S, { kind: 'completion', startedAt: Date.now(), line: pickCarthageLine() });
108
+ S.runCompletionPending = false;
109
+ }
110
+ S.version++;
111
+ break;
112
+ case 'turn_start':
113
+ S.turns++; S.version++;
114
+ break;
115
+ case 'message_start':
116
+ if (ev.message?.role === 'user') {
117
+ const text = textOf(ev.message.content);
118
+ const last = S.blocks[S.blocks.length - 1];
119
+ // dedupe our local echo (same text just submitted shows up again via events)
120
+ if (last?.kind === 'user' && (last.text === text || last.expanded === text) && Date.now() - last.t < 15000) {
121
+ last.queued = false; S.version++;
122
+ } else {
123
+ push(S, { kind: 'user', text });
124
+ }
125
+ }
126
+ // assistant blocks are created lazily on the first text delta so
127
+ // tool-call-only messages don't leave an empty '●' line in the feed
128
+ break;
129
+ case 'message_update':
130
+ if (!ase) break;
131
+ if (ase.type === 'text_delta' || ase.type === 'thinking_delta') {
132
+ const isThink = ase.type === 'thinking_delta';
133
+ let b = isThink ? S.currentThinking : S.currentAssistant;
134
+ if (!b) {
135
+ b = push(S, { kind: isThink ? 'thinking' : 'assistant', text: '', streaming: true });
136
+ if (isThink) S.currentThinking = b; else S.currentAssistant = b;
137
+ }
138
+ b.text += ase.delta;
139
+ b.streaming = true;
140
+ S.version++;
141
+ } else if (ase.type === 'toolcall_start') {
142
+ const b = push(S, { kind: 'tool', id: ase.id, name: ase.toolName || 'tool', args: null, status: 'calling', tail: [], live: '' });
143
+ if (ase.id) S.toolBlocks.set(ase.id, b);
144
+ } else if (ase.type === 'toolcall_end' && ase.toolCall) {
145
+ const b = S.toolBlocks.get(ase.toolCall.id);
146
+ if (b) { b.args = ase.toolCall.arguments; S.version++; }
147
+ }
148
+ break;
149
+ case 'message_end':
150
+ if (ev.message?.role === 'assistant') {
151
+ if (S.currentAssistant) S.currentAssistant.streaming = false;
152
+ if (S.currentThinking) S.currentThinking.streaming = false;
153
+ S.currentAssistant = null; S.currentThinking = null;
154
+ const u = ev.message.usage;
155
+ if (u) { S.tokens += u.totalTokens || 0; S.cost += u.cost?.total || 0; }
156
+ if (ev.message.stopReason === 'error') {
157
+ push(S, { kind: 'notice', tone: 'error', text: `provider error: ${ev.message.errorMessage || 'unknown'}` });
158
+ }
159
+ S.version++;
160
+ }
161
+ break;
162
+ case 'tool_execution_start': {
163
+ let b = S.toolBlocks.get(ev.toolCallId);
164
+ if (!b) {
165
+ b = push(S, { kind: 'tool', id: ev.toolCallId, name: ev.toolName, args: ev.args, status: 'running', tail: [], live: '' });
166
+ S.toolBlocks.set(ev.toolCallId, b);
167
+ }
168
+ b.name = ev.toolName; b.args = ev.args; b.status = 'running';
169
+ S.toolCalls++; S.version++;
170
+ break;
171
+ }
172
+ case 'tool_execution_update': {
173
+ const b = S.toolBlocks.get(ev.toolCallId);
174
+ if (b) { b.live = resultText(ev.partialResult) || b.live; S.version++; }
175
+ break;
176
+ }
177
+ case 'tool_execution_end': {
178
+ const b = S.toolBlocks.get(ev.toolCallId);
179
+ if (!b) break;
180
+ b.status = ev.isError ? 'error' : 'done';
181
+ b.end = Date.now();
182
+ const out = resultText(ev.result);
183
+ b.tail = tailLines(out, 40);
184
+ b.rawResult = out;
185
+ S.version++;
186
+ break;
187
+ }
188
+ case 'auto_retry_start':
189
+ push(S, { kind: 'notice', tone: 'warn', text: `provider error — retry ${ev.attempt}/${ev.maxAttempts} in ${Math.round((ev.delayMs || 0) / 1000)}s${ev.errorMessage ? ` · ${ev.errorMessage}` : ''}` });
190
+ break;
191
+ case 'auto_retry_end':
192
+ if (!ev.success) push(S, { kind: 'notice', tone: 'error', text: `retry failed${ev.finalError ? `: ${ev.finalError}` : ''}` });
193
+ break;
194
+ case 'compaction_start':
195
+ push(S, { kind: 'notice', tone: 'info', text: `compacting context (${ev.reason})…` });
196
+ break;
197
+ case 'compaction_end':
198
+ push(S, { kind: 'notice', tone: ev.errorMessage ? 'error' : 'info', text: ev.aborted ? 'compaction aborted' : ev.errorMessage ? `compaction failed — ${ev.errorMessage}` : 'context compacted' });
199
+ break;
200
+ case 'agent_end':
201
+ S.running = false; S.runCompletionPending = true; S.version++;
202
+ break;
203
+ case 'extension_ui_request':
204
+ if (ev.method === 'notify') {
205
+ push(S, { kind: 'notice', tone: ev.notifyType === 'warning' ? 'warn' : ev.notifyType || 'info', text: ev.message });
206
+ } else if (ev.method === 'setStatus' && ev.statusKey === 'delivery') {
207
+ S.deliveryStatusText = ev.statusText; S.version++;
208
+ }
209
+ break;
210
+ case 'bash_execution_update': {
211
+ const b = S.userBash;
212
+ if (b) { b.live = (b.live || '') + (ev.delta || ''); S.version++; }
213
+ break;
214
+ }
215
+ case 'response':
216
+ if (ev.success === false) push(S, { kind: 'notice', tone: 'error', text: `${ev.command || 'rpc'}: ${ev.error}` });
217
+ break;
218
+ }
219
+ return S;
220
+ }
221
+
222
+ /** Local echo helpers (things we send, so the feed shows them instantly). */
223
+ export function echoUser(S, text, queued, expanded) {
224
+ // Phased delivery is the carthagent default — no badge on framed prompts.
225
+ push(S, { kind: 'user', text, queued, expanded, via: undefined });
226
+ }
227
+ /** Run separator, pushed before a user echo so the feed reads runline → user → …. */
228
+ export function runline(S) {
229
+ if (S.blocks.length && S.blocks[S.blocks.length - 1].kind !== 'runline') push(S, { kind: 'runline' });
230
+ }
231
+ export function echoBash(S, command) {
232
+ S.userBash = push(S, { kind: 'bash', command, status: 'running', live: '', tail: [] });
233
+ return S.userBash;
234
+ }
235
+ export function notice(S, text, tone = 'info') {
236
+ push(S, { kind: 'notice', tone, text });
237
+ }
238
+
239
+ /**
240
+ * Rebuild the feed from a saved session's entries (parsed session .jsonl lines).
241
+ * Replaces S.blocks so a resumed session shows its history. Framed prompts
242
+ * are unwrapped back to the task text the user originally typed.
243
+ */
244
+ export function hydrateFeed(S, entries, { max = 400 } = {}) {
245
+ const blocks = [];
246
+ const tools = new Map();
247
+ const put = b => { b.t = Date.now(); blocks.push(b); return b; };
248
+ let tokens = 0, cost = 0, toolCalls = 0;
249
+ for (const e of entries) {
250
+ if (e?.type !== 'message' || !e.message) continue;
251
+ const m = e.message;
252
+ if (m.role === 'user') {
253
+ const raw = textOf(m.content).trim();
254
+ if (!raw) continue;
255
+ const task = unframe(raw);
256
+ put({ kind: 'user', text: task || raw, via: undefined, expanded: task ? raw : undefined });
257
+ } else if (m.role === 'assistant') {
258
+ for (const c of m.content || []) {
259
+ if (c?.type === 'text' && c.text) put({ kind: 'assistant', text: c.text, streaming: false });
260
+ else if (c?.type === 'thinking' && (c.thinking || c.text)) put({ kind: 'thinking', text: c.thinking || c.text, streaming: false });
261
+ else if (c?.type === 'toolCall') {
262
+ const b = put({ kind: 'tool', id: c.id, name: c.name || 'tool', args: c.arguments || null, status: 'done', tail: [] });
263
+ if (c.id) tools.set(c.id, b);
264
+ toolCalls++;
265
+ }
266
+ }
267
+ if (m.usage) { tokens += m.usage.totalTokens || 0; cost += m.usage.cost?.total || 0; }
268
+ } else if (m.role === 'toolResult') {
269
+ const b = tools.get(m.toolCallId);
270
+ if (b) {
271
+ b.status = m.isError ? 'error' : 'done';
272
+ const out = textOf(m.content);
273
+ b.tail = tailLines(out, 40);
274
+ b.rawResult = out;
275
+ }
276
+ }
277
+ }
278
+ S.blocks = blocks.slice(-max);
279
+ S.toolBlocks = tools;
280
+ S.tokens = tokens;
281
+ S.cost = cost;
282
+ S.toolCalls = toolCalls;
283
+ S.currentAssistant = null;
284
+ S.currentThinking = null;
285
+ S.version++;
286
+ }
287
+
288
+ // ---------------------------------------------------------------- rendering
289
+
290
+ export function summarizeArgs(name, args) {
291
+ if (!args || typeof args !== 'object') return '';
292
+ const clip = (s, n) => { s = String(s ?? ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
293
+ switch (name) {
294
+ case 'read': return clip(`${args.path ?? ''}${args.offset ? ` :${args.offset}` : ''}`, 80);
295
+ case 'write': return clip(`${args.path ?? ''} · ${(args.content ?? '').length}B`, 80);
296
+ case 'edit': return clip(`${args.path ?? ''} · ${Array.isArray(args.edits) ? args.edits.length : '?'} edits`, 80);
297
+ case 'bash': case 'powershell': return clip(String(args.command ?? '').split('\n')[0], 90);
298
+ case 'grep': return clip(`/${args.pattern ?? ''}/ ${args.path ?? ''}`, 80);
299
+ case 'find': return clip(`${args.pattern ?? ''} ${args.path ?? ''}`, 80);
300
+ case 'ls': return clip(args.path ?? '.', 80);
301
+ case 'delivery_plan': return clip(args.goal ?? '', 90);
302
+ case 'delivery_revise': return clip(args.reason ?? '', 90);
303
+ case 'delivery_design':
304
+ case 'delivery_review': return clip(args.action ?? '', 90);
305
+ case 'delivery_check': return `id=${args.id ?? 'all'}`;
306
+ case 'delivery_finish': return `status=${args.status ?? '?'}`;
307
+ case 'delivery_status': return '';
308
+ default: {
309
+ try { return clip(JSON.stringify(args), 80); } catch { return ''; }
310
+ }
311
+ }
312
+ }
313
+
314
+ /** Tiny inline markdown: **bold**, `code`, headings. Returns styled string.
315
+ * Uses opencode markdown tokens with legacy carthagent fallbacks (accent2/accent). */
316
+ export function mdInline(text, T) {
317
+ const code = T.markdownCode || T.accent2 || T.secondary;
318
+ const accent = T.markdownHeading || T.accent || T.primary;
319
+ const link = T.markdownLink || accent;
320
+ let s = String(text);
321
+ s = s.replace(/`([^`\n]+)`/g, (_, c) => fg(code) + c + RESET + fg(T.text));
322
+ s = s.replace(/\*\*([^*\n]+)\*\*/g, (_, c) => BOLD + c + RESET + fg(T.text));
323
+ s = s.replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, (_, t, u) => fg(link) + t + RESET + fg(T.textMuted || T.faint) + ` (${u})` + RESET + fg(T.text));
324
+ s = s.replace(/^#{1,4}\s*(.*)$/, (_, c) => BOLD + fg(accent) + c + RESET + fg(T.text));
325
+ return s;
326
+ }
327
+
328
+ const BLOCK_GAP = 1;
329
+
330
+ /**
331
+ * Render all blocks to styled lines for the feed pane.
332
+ * Returns { lines, blockRows } — blockRows[i] = starting row of block i.
333
+ */
334
+ export function renderFeed(S, W, T, { now = Date.now(), expandResults = false, frame = 0, glyphs = UNICODE_GLYPHS } = {}) {
335
+ const G = glyphs;
336
+ const spin = f => spinnerFrame(G, f);
337
+ const lines = [];
338
+ const blockRows = [];
339
+ const inner = Math.max(10, W - 4);
340
+ const fade = (styled, block) => (now - block.t < 350 ? DIM + styled : styled);
341
+
342
+ for (const b of S.blocks) {
343
+ blockRows.push(lines.length);
344
+ const body = [];
345
+ const faint = T.textMuted || T.faint, muted = T.muted || faint;
346
+ const accent = T.primary || T.accent, accent2 = T.secondary || T.accent2 || accent;
347
+ const codeTok = T.markdownCode || accent2;
348
+ switch (b.kind) {
349
+ case 'runline': {
350
+ const label = ` run${b.n != null ? ` ${b.n}` : ''} `;
351
+ const side = Math.max(2, Math.floor((inner - label.length - 2) / 2));
352
+ body.push(`${fg(T.borderSubtle || faint)}${'─'.repeat(side)}${RESET}${fg(muted)}${label}${RESET}${fg(T.borderSubtle || faint)}${'─'.repeat(Math.max(0, inner - side - label.length))}${RESET}`);
353
+ break;
354
+ }
355
+ case 'user': {
356
+ body.push(`${fg(accent2)}${BOLD}▌ you${RESET}${b.via ? ` ${fg(faint)}${G.right} ${b.via}${RESET}` : ''}`);
357
+ for (const l of wrap(b.text, inner)) body.push(`${fg(T.text)}${l}${RESET}`);
358
+ if (b.queued) body.push(`${fg(faint)}${ITALIC}queued while agent is running…${RESET}`);
359
+ break;
360
+ }
361
+ case 'assistant': {
362
+ const inFence = { v: false };
363
+ let first = true;
364
+ for (const raw of b.text.split('\n')) {
365
+ if (/^```/.test(raw.trim())) { inFence.v = !inFence.v; body.push(`${fg(T.borderSubtle || faint)}│${RESET}`); continue; }
366
+ const styled = inFence.v ? `${fg(codeTok)} ${raw}${RESET}` : fg(T.text) + mdInline(raw, T) + RESET;
367
+ for (const l of wrap(styled, inner)) {
368
+ if (first && !inFence.v) { body.push(`${fg(accent)}${G.phaseDone}${RESET} ${l}`); first = false; }
369
+ else body.push(l);
370
+ }
371
+ if (first && inFence.v) first = false;
372
+ }
373
+ if (!b.text && b.streaming) { /* spinner below */ }
374
+ if (b.streaming) body.push(`${fg(accent)}${spin(frame)}${RESET}`);
375
+ break;
376
+ }
377
+ case 'thinking': {
378
+ body.push(`${fg(faint)}${ITALIC}${G.info} thinking${RESET}`);
379
+ const t = tailLines(b.text, expandResults ? 40 : 4).join('\n');
380
+ for (const l of wrap(t, inner)) body.push(`${fg(faint)}${ITALIC}${l}${RESET}`);
381
+ if (b.streaming) body.push(`${fg(faint)}${ITALIC}${spin(frame)}${RESET}`);
382
+ break;
383
+ }
384
+ case 'tool': {
385
+ const icon = toolIcon(G, b.name);
386
+ const accentHot = T.accent || accent;
387
+ const status = b.status === 'running' ? `${fg(accent)}${spin(frame)}${RESET}`
388
+ : b.status === 'done' ? `${fg(T.diffAdded || T.ok)}${G.done}${RESET}`
389
+ : b.status === 'error' ? `${fg(T.diffRemoved || T.err)}${G.fail}${RESET}`
390
+ : `${fg(faint)}${G.ellipsis}${RESET}`;
391
+ const summary = summarizeArgs(b.name, b.args);
392
+ body.push(`${fg(accentHot)}${icon}${RESET} ${fg(accent)}${BOLD}${b.name}${RESET} ${fg(muted)}${summary}${RESET} ${status}`);
393
+ if (b.live && b.status === 'running') {
394
+ for (const l of tailLines(b.live, 3)) for (const w of wrap(l, inner - 2)) body.push(` ${fg(muted)}${w}${RESET}`);
395
+ }
396
+ const show = expandResults ? b.tail : b.tail.slice(-3);
397
+ if (b.status !== 'running' && show.length) {
398
+ const color = b.status === 'error' ? (T.diffRemoved || T.error || T.err) : (T.diffContext || faint);
399
+ // Error tails get an opencode-style tinted gutter.
400
+ const gutterBg = b.status === 'error' && T.diffRemovedBg ? true : false;
401
+ let firstTail = true;
402
+ for (const l of show) for (const w of wrap(l, inner - 2)) {
403
+ const mark = firstTail ? fg(faint) + G.tail + ' ' + RESET : ' ';
404
+ const line = gutterBg ? fg(T.diffRemoved || T.error || T.err) + '▌' + RESET + ' ' + fg(color) + w + RESET : `${fg(color)}${w}${RESET}`;
405
+ body.push(`${mark}${line}`);
406
+ firstTail = false;
407
+ }
408
+ if (!expandResults && b.tail.length > 3) {
409
+ for (const w of wrap(`… ${b.tail.length - 3} more lines (^x to expand)`, inner - 2)) body.push(` ${fg(faint)}${w}${RESET}`);
410
+ }
411
+ }
412
+ break;
413
+ }
414
+ case 'bash': {
415
+ body.push(`${fg(T.warning || T.warn)}${BOLD}$${RESET} ${fg(T.text)}${b.command}${RESET}`);
416
+ const out = b.status === 'running' ? (b.live || '') : (b.tail?.join('\n') || '');
417
+ for (const l of tailLines(out, expandResults ? 30 : 5)) for (const w of wrap(l, inner - 2)) body.push(` ${fg(muted)}${w}${RESET}`);
418
+ if (b.status === 'running') body.push(` ${fg(accent)}${spin(frame)}${RESET}`);
419
+ else if (b.code != null) body.push(` ${fg(b.code === 0 ? (T.diffAdded || T.success || T.ok) : (T.diffRemoved || T.error || T.err))}exit ${b.code}${RESET}`);
420
+ break;
421
+ }
422
+ case 'notice': {
423
+ const c = b.tone === 'error' ? (T.error || T.err) : b.tone === 'warn' ? (T.warning || T.warn) : faint;
424
+ const sym = b.tone === 'error' ? G.fail : b.tone === 'warn' ? G.warn : G.info;
425
+ for (const l of wrap(b.text, inner)) body.push(`${fg(c)}${sym} ${l}${RESET}`);
426
+ break;
427
+ }
428
+ case 'banner': {
429
+ const c = b.tone === 'ok' ? (T.success || T.ok) : b.tone === 'error' ? (T.error || T.err) : (T.warning || T.warn);
430
+ body.push(`${fg(c)}${BOLD}${'─'.repeat(4)} ${b.title} ${'─'.repeat(4)}${RESET}`);
431
+ for (const l of (b.text || '').split('\n')) for (const w of wrap(l, inner)) body.push(`${fg(muted)}${w}${RESET}`);
432
+ break;
433
+ }
434
+ case 'completion': {
435
+ const age = Math.max(0, now - (b.startedAt || b.t));
436
+ const beat = Math.min(1, age / 720);
437
+ const pulse = [0.25, 0.55, 0.9, 0.55, 0.25][Math.min(4, Math.floor(age / 145))];
438
+ const gold = T.success || T.ok || accent;
439
+ const star = age < 720 ? (frame % 2 ? G.sparkle : G.sparkleHollow) : G.sparkle;
440
+ const line = b.line || CARTHAGE_LINES[Math.abs(b.t || 0) % CARTHAGE_LINES.length];
441
+ const message = `${star} Run complete ${G.aleph || 'A'} ${line} ${star}`;
442
+ body.push(`${fg(mix(gold, '#ffffff', pulse * (1 - beat * 0.45)))}${BOLD}${message}${RESET}`);
443
+ break;
444
+ }
445
+ case 'welcome': {
446
+ body.push(`${fg(accent)}${BOLD}carthagent${RESET}`);
447
+ if (b.noProvider) {
448
+ body.push(`${fg(T.warning || T.warn || accent2)}No AI provider connected.${RESET}`);
449
+ body.push(`${fg(faint)}${truncate(b.cwd || '', inner)}${RESET}`);
450
+ body.push('');
451
+ body.push(`${fg(muted)}Connect a provider to get started.${RESET}`);
452
+ body.push(`${fg(accent2)}Sign in to Carthagent Ship to start with trial credit.${RESET}`);
453
+ body.push(`${fg(muted)}Press /login or Esc to dismiss this popup.${RESET}`);
454
+ } else {
455
+ body.push(`${fg(faint)}${truncate(b.model || 'default model', inner)}${RESET}`);
456
+ body.push(`${fg(faint)}${truncate(b.cwd || '', inner)}${RESET}`);
457
+ body.push('');
458
+ body.push(`${fg(muted)}Enter a message. Use /help for commands.${RESET}`);
459
+ }
460
+ break;
461
+ }
462
+ }
463
+ for (const l of body) lines.push(fade(l, b));
464
+ for (let i = 0; i < BLOCK_GAP; i++) lines.push('');
465
+ }
466
+ return { lines, blockRows };
467
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Behind-the-scenes phased-delivery framing (the carthagent default flow).
3
+ *
4
+ * Every typed task is a phased delivery run: the raw text is what the user
5
+ * sees echoed in the feed, while the engine receives the expanded delivery template
6
+ * body. Resolving the template here (rather than forwarding "/guide <task>"
7
+ * to the engine) keeps the behavior identical in --isolate and headless -p modes,
8
+ * and keeps the machinery invisible in the feed.
9
+ *
10
+ * This is the default flow for carthagent — the whole product is built around it —
11
+ * so the input pill stays empty and the hint rotates through generic
12
+ * builder one-liners.
13
+ */
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import { join, dirname, resolve } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const ROOT = resolve(__dirname, '..', '..');
20
+ const TEMPLATE_PATH = join(ROOT, 'prompts', 'delivery.md');
21
+
22
+ let cached = null;
23
+
24
+ export const FRAME_HINTS = ['Message carthagent…'];
25
+
26
+ export function frameHint() {
27
+ return FRAME_HINTS[0];
28
+ }
29
+
30
+ function readTemplate(path) {
31
+ let body = '';
32
+ try {
33
+ const raw = readFileSync(path, 'utf8');
34
+ const fm = raw.match(/^---\n[\s\S]*?\n---\n?/);
35
+ body = (fm ? raw.slice(fm[0].length) : raw).trim();
36
+ } catch { body = ''; }
37
+ return body;
38
+ }
39
+
40
+ /** Body of prompts/delivery.md with frontmatter stripped. '' when unreadable. */
41
+ export function framingTemplate(path = TEMPLATE_PATH) {
42
+ if (path === TEMPLATE_PATH) {
43
+ if (cached == null) cached = readTemplate(TEMPLATE_PATH);
44
+ return cached;
45
+ }
46
+ return readTemplate(path);
47
+ }
48
+
49
+ /** Expand the delivery template for a task. Falls back to the bare task text. */
50
+ export function framePrompt(task, template = framingTemplate()) {
51
+ const t = String(task).trim();
52
+ if (!template) return t;
53
+ return template.includes('$@') ? template.replaceAll('$@', t) : `${template}\n\n${t}`;
54
+ }
55
+
56
+ /** Recover the original task text from a framed prompt, else null. */
57
+ export function unframe(text) {
58
+ // Match the current template suffix first: the task itself may contain
59
+ // blank lines (or even phase headings). Keep the legacy format fallback.
60
+ const value = String(text);
61
+ const template = framingTemplate();
62
+ const parts = template.split('$@');
63
+ if (parts.length === 2 && value.startsWith(parts[0]) && value.endsWith(parts[1])) {
64
+ return value.slice(parts[0].length, parts[1] ? -parts[1].length : undefined).trim();
65
+ }
66
+ const m = value.match(/^Deliver this task in phases(?: with executable evidence)?:\s*([\s\S]*?)\n\n/);
67
+ return m ? m[1].trim() : null;
68
+ }
69
+
70
+ /** True when a submitted prompt should get the delivery framing. */
71
+ export function shouldFrame(text, { enabled = true, delivery = true } = {}) {
72
+ if (!enabled || !delivery) return false;
73
+ const v = String(text).trim();
74
+ if (!v) return false;
75
+ if (v.startsWith('/') || v.startsWith('!')) return false;
76
+ // Questions/read-only asks are not delivery runs.
77
+ if (v.endsWith('?') && v.length < 300) return false;
78
+ if (/^(what|why|how|explain|show|list|describe|review|where|is|are|does|do|can)\b/i.test(v) && v.length < 300 && !/\b(build|create|implement|write|add|fix|make|generate)\b/i.test(v)) return false;
79
+ return existsSync(TEMPLATE_PATH);
80
+ }