gsd-pi 2.18.0 → 2.20.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 (289) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +3 -3
  3. package/dist/onboarding.d.ts +3 -1
  4. package/dist/onboarding.js +77 -3
  5. package/dist/remote-questions-config.d.ts +1 -1
  6. package/dist/resources/extensions/google-search/index.ts +164 -47
  7. package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +148 -39
  9. package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
  10. package/dist/resources/extensions/gsd/auto.ts +690 -39
  11. package/dist/resources/extensions/gsd/captures.ts +384 -0
  12. package/dist/resources/extensions/gsd/commands.ts +654 -36
  13. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  14. package/dist/resources/extensions/gsd/context-budget.ts +243 -0
  15. package/dist/resources/extensions/gsd/context-store.ts +195 -0
  16. package/dist/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  17. package/dist/resources/extensions/gsd/db-writer.ts +341 -0
  18. package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
  19. package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
  20. package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  21. package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
  22. package/dist/resources/extensions/gsd/doctor.ts +283 -2
  23. package/dist/resources/extensions/gsd/export.ts +81 -2
  24. package/dist/resources/extensions/gsd/files.ts +39 -9
  25. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  26. package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
  27. package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
  28. package/dist/resources/extensions/gsd/history.ts +0 -1
  29. package/dist/resources/extensions/gsd/index.ts +277 -1
  30. package/dist/resources/extensions/gsd/md-importer.ts +526 -0
  31. package/dist/resources/extensions/gsd/metrics.ts +84 -0
  32. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  33. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  34. package/dist/resources/extensions/gsd/notifications.ts +0 -1
  35. package/dist/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  36. package/dist/resources/extensions/gsd/preferences.ts +198 -150
  37. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
  39. package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  40. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  41. package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
  42. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  43. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  44. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  45. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  46. package/dist/resources/extensions/gsd/quick.ts +156 -0
  47. package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
  48. package/dist/resources/extensions/gsd/skill-health.ts +417 -0
  49. package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
  50. package/dist/resources/extensions/gsd/state.ts +30 -0
  51. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  52. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  53. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  54. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  55. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  57. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  58. package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  59. package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  60. package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  61. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  62. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  63. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  64. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  65. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  66. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  67. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  68. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  69. package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  70. package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  71. package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  72. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  73. package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  74. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  75. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  76. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  77. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  78. package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  79. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  80. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  81. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  82. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  83. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  84. package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  85. package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  86. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  87. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  88. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  89. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  90. package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  91. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  92. package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  93. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  94. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  95. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  96. package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  97. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  98. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  99. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  100. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  101. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  102. package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  103. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  104. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  105. package/dist/resources/extensions/gsd/types.ts +29 -0
  106. package/dist/resources/extensions/gsd/undo.ts +0 -1
  107. package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
  108. package/dist/resources/extensions/gsd/visualizer-data.ts +505 -0
  109. package/dist/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  110. package/dist/resources/extensions/gsd/visualizer-views.ts +755 -0
  111. package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
  112. package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
  113. package/dist/resources/extensions/remote-questions/config.ts +4 -2
  114. package/dist/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  115. package/dist/resources/extensions/remote-questions/format.ts +166 -14
  116. package/dist/resources/extensions/remote-questions/manager.ts +14 -4
  117. package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
  118. package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  119. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  120. package/dist/resources/extensions/remote-questions/types.ts +2 -1
  121. package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  122. package/dist/resources/extensions/voice/index.ts +4 -3
  123. package/package.json +1 -1
  124. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
  126. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  129. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
  131. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
  133. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
  135. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
  137. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
  139. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
  140. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
  142. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
  144. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
  146. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  148. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
  150. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  152. package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
  153. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  154. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
  156. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  158. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  160. package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
  161. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  162. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
  163. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  164. package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
  165. package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
  166. package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
  167. package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
  168. package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
  169. package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
  170. package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
  171. package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
  172. package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
  173. package/src/resources/extensions/google-search/index.ts +164 -47
  174. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  175. package/src/resources/extensions/gsd/auto-prompts.ts +148 -39
  176. package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
  177. package/src/resources/extensions/gsd/auto.ts +690 -39
  178. package/src/resources/extensions/gsd/captures.ts +384 -0
  179. package/src/resources/extensions/gsd/commands.ts +654 -36
  180. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  181. package/src/resources/extensions/gsd/context-budget.ts +243 -0
  182. package/src/resources/extensions/gsd/context-store.ts +195 -0
  183. package/src/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  184. package/src/resources/extensions/gsd/db-writer.ts +341 -0
  185. package/src/resources/extensions/gsd/debug-logger.ts +178 -0
  186. package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
  187. package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  188. package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
  189. package/src/resources/extensions/gsd/doctor.ts +283 -2
  190. package/src/resources/extensions/gsd/export.ts +81 -2
  191. package/src/resources/extensions/gsd/files.ts +39 -9
  192. package/src/resources/extensions/gsd/git-service.ts +6 -0
  193. package/src/resources/extensions/gsd/gsd-db.ts +752 -0
  194. package/src/resources/extensions/gsd/guided-flow.ts +26 -1
  195. package/src/resources/extensions/gsd/history.ts +0 -1
  196. package/src/resources/extensions/gsd/index.ts +277 -1
  197. package/src/resources/extensions/gsd/md-importer.ts +526 -0
  198. package/src/resources/extensions/gsd/metrics.ts +84 -0
  199. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  200. package/src/resources/extensions/gsd/model-router.ts +256 -0
  201. package/src/resources/extensions/gsd/notifications.ts +0 -1
  202. package/src/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  203. package/src/resources/extensions/gsd/preferences.ts +198 -150
  204. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  205. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
  206. package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  207. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  208. package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
  209. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  210. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  211. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  212. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  213. package/src/resources/extensions/gsd/quick.ts +156 -0
  214. package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
  215. package/src/resources/extensions/gsd/skill-health.ts +417 -0
  216. package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
  217. package/src/resources/extensions/gsd/state.ts +30 -0
  218. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  219. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  220. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  221. package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  222. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  223. package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  224. package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  225. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  226. package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  227. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  228. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  229. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  230. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  231. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  232. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  233. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  234. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  235. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  236. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  237. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  238. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  239. package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  240. package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  241. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  242. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  243. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  244. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  245. package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  246. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  247. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  248. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  249. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  250. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  251. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  252. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  253. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  254. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  255. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  256. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  257. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  258. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  259. package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  260. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  261. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  262. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  263. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  264. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  265. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  266. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  267. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  268. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  269. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  270. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  271. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  272. package/src/resources/extensions/gsd/types.ts +29 -0
  273. package/src/resources/extensions/gsd/undo.ts +0 -1
  274. package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
  275. package/src/resources/extensions/gsd/visualizer-data.ts +505 -0
  276. package/src/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  277. package/src/resources/extensions/gsd/visualizer-views.ts +755 -0
  278. package/src/resources/extensions/gsd/worktree-command.ts +18 -0
  279. package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
  280. package/src/resources/extensions/remote-questions/config.ts +4 -2
  281. package/src/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  282. package/src/resources/extensions/remote-questions/format.ts +166 -14
  283. package/src/resources/extensions/remote-questions/manager.ts +14 -4
  284. package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
  285. package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  286. package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  287. package/src/resources/extensions/remote-questions/types.ts +2 -1
  288. package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  289. package/src/resources/extensions/voice/index.ts +4 -3
@@ -0,0 +1,755 @@
1
+ // View renderers for the GSD workflow visualizer overlay.
2
+
3
+ import type { Theme } from "@gsd/pi-coding-agent";
4
+ import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
5
+ import type { VisualizerData, VisualizerMilestone } from "./visualizer-data.js";
6
+ import { formatCost, formatTokenCount, classifyUnitPhase } from "./metrics.js";
7
+
8
+ // ─── Local Helpers ───────────────────────────────────────────────────────────
9
+
10
+ function formatDuration(ms: number): string {
11
+ const s = Math.floor(ms / 1000);
12
+ if (s < 60) return `${s}s`;
13
+ const m = Math.floor(s / 60);
14
+ const rs = s % 60;
15
+ if (m < 60) return `${m}m ${rs}s`;
16
+ const h = Math.floor(m / 60);
17
+ const rm = m % 60;
18
+ return `${h}h ${rm}m`;
19
+ }
20
+
21
+ function padRight(content: string, width: number): string {
22
+ const vis = visibleWidth(content);
23
+ return content + " ".repeat(Math.max(0, width - vis));
24
+ }
25
+
26
+ function joinColumns(left: string, right: string, width: number): string {
27
+ const leftW = visibleWidth(left);
28
+ const rightW = visibleWidth(right);
29
+ if (leftW + rightW + 2 > width) {
30
+ return truncateToWidth(`${left} ${right}`, width);
31
+ }
32
+ return left + " ".repeat(width - leftW - rightW) + right;
33
+ }
34
+
35
+ function sparkline(values: number[]): string {
36
+ if (values.length === 0) return "";
37
+ const chars = "▁▂▃▄▅▆▇█";
38
+ const max = Math.max(...values);
39
+ if (max === 0) return chars[0].repeat(values.length);
40
+ return values.map(v => chars[Math.min(7, Math.floor((v / max) * 7))]).join("");
41
+ }
42
+
43
+ // ─── Progress View ───────────────────────────────────────────────────────────
44
+
45
+ export interface ProgressFilter {
46
+ text: string;
47
+ field: "all" | "status" | "risk" | "keyword";
48
+ }
49
+
50
+ export function renderProgressView(
51
+ data: VisualizerData,
52
+ th: Theme,
53
+ width: number,
54
+ filter?: ProgressFilter,
55
+ ): string[] {
56
+ const lines: string[] = [];
57
+
58
+ // Risk Heatmap
59
+ lines.push(...renderRiskHeatmap(data, th, width));
60
+ if (data.milestones.length > 0) lines.push("");
61
+
62
+ // Filter indicator
63
+ if (filter && filter.text) {
64
+ lines.push(th.fg("accent", `Filter (${filter.field}): ${filter.text}`));
65
+ lines.push("");
66
+ }
67
+
68
+ for (const ms of data.milestones) {
69
+ // Apply filter to milestones
70
+ if (filter && filter.text) {
71
+ const matchesMs = matchesFilter(ms, filter);
72
+ if (!matchesMs) continue;
73
+ }
74
+
75
+ // Milestone header line
76
+ const statusGlyph =
77
+ ms.status === "complete"
78
+ ? th.fg("success", "✓")
79
+ : ms.status === "active"
80
+ ? th.fg("accent", "▸")
81
+ : th.fg("dim", "○");
82
+ const statusLabel =
83
+ ms.status === "complete"
84
+ ? th.fg("success", "complete")
85
+ : ms.status === "active"
86
+ ? th.fg("accent", "active")
87
+ : th.fg("dim", "pending");
88
+ const msLeft = `${ms.id}: ${ms.title}`;
89
+ const msRight = `${statusGlyph} ${statusLabel}`;
90
+ lines.push(joinColumns(msLeft, msRight, width));
91
+
92
+ if (ms.slices.length === 0 && ms.dependsOn.length > 0) {
93
+ lines.push(th.fg("dim", ` (depends on ${ms.dependsOn.join(", ")})`));
94
+ continue;
95
+ }
96
+
97
+ if (ms.status === "pending" && ms.dependsOn.length > 0) {
98
+ lines.push(th.fg("dim", ` (depends on ${ms.dependsOn.join(", ")})`));
99
+ continue;
100
+ }
101
+
102
+ for (const sl of ms.slices) {
103
+ // Apply filter to slices
104
+ if (filter && filter.text) {
105
+ if (!matchesSliceFilter(sl, filter)) continue;
106
+ }
107
+
108
+ // Slice line
109
+ const slGlyph = sl.done
110
+ ? th.fg("success", "✓")
111
+ : sl.active
112
+ ? th.fg("accent", "▸")
113
+ : th.fg("dim", "○");
114
+ const riskColor =
115
+ sl.risk === "high"
116
+ ? "warning"
117
+ : sl.risk === "medium"
118
+ ? "text"
119
+ : "dim";
120
+ const riskBadge = th.fg(riskColor, sl.risk);
121
+ const slLeft = ` ${slGlyph} ${sl.id}: ${sl.title}`;
122
+ lines.push(joinColumns(slLeft, riskBadge, width));
123
+
124
+ // Show tasks for active slice
125
+ if (sl.active && sl.tasks.length > 0) {
126
+ for (const task of sl.tasks) {
127
+ const tGlyph = task.done
128
+ ? th.fg("success", "✓")
129
+ : task.active
130
+ ? th.fg("accent", "▸")
131
+ : th.fg("dim", "○");
132
+ lines.push(` ${tGlyph} ${task.id}: ${task.title}`);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ return lines;
139
+ }
140
+
141
+ function matchesFilter(ms: VisualizerMilestone, filter: ProgressFilter): boolean {
142
+ const text = filter.text.toLowerCase();
143
+ if (filter.field === "status") {
144
+ return ms.status.includes(text);
145
+ }
146
+ if (filter.field === "risk") {
147
+ return ms.slices.some(s => s.risk.toLowerCase().includes(text));
148
+ }
149
+ // "all" or "keyword"
150
+ if (ms.id.toLowerCase().includes(text)) return true;
151
+ if (ms.title.toLowerCase().includes(text)) return true;
152
+ if (ms.status.includes(text)) return true;
153
+ return ms.slices.some(s => matchesSliceFilter(s, filter));
154
+ }
155
+
156
+ function matchesSliceFilter(sl: { id: string; title: string; risk: string }, filter: ProgressFilter): boolean {
157
+ const text = filter.text.toLowerCase();
158
+ if (filter.field === "status") return true; // slices don't have named status
159
+ if (filter.field === "risk") return sl.risk.toLowerCase().includes(text);
160
+ return sl.id.toLowerCase().includes(text) ||
161
+ sl.title.toLowerCase().includes(text) ||
162
+ sl.risk.toLowerCase().includes(text);
163
+ }
164
+
165
+ // ─── Risk Heatmap ────────────────────────────────────────────────────────────
166
+
167
+ function renderRiskHeatmap(data: VisualizerData, th: Theme, width: number): string[] {
168
+ const allSlices = data.milestones.flatMap(m => m.slices);
169
+ if (allSlices.length === 0) return [];
170
+
171
+ const lines: string[] = [];
172
+ lines.push(th.fg("accent", th.bold("Risk Heatmap")));
173
+ lines.push("");
174
+
175
+ for (const ms of data.milestones) {
176
+ if (ms.slices.length === 0) continue;
177
+ const blocks = ms.slices.map(s => {
178
+ const color = s.risk === "high" ? "error" : s.risk === "medium" ? "warning" : "success";
179
+ return th.fg(color, "██");
180
+ });
181
+ const row = ` ${padRight(ms.id, 6)} ${blocks.join(" ")}`;
182
+ lines.push(truncateToWidth(row, width));
183
+ }
184
+
185
+ lines.push("");
186
+ lines.push(
187
+ ` ${th.fg("success", "██")} low ${th.fg("warning", "██")} med ${th.fg("error", "██")} high`,
188
+ );
189
+
190
+ // Summary counts
191
+ let low = 0, med = 0, high = 0;
192
+ let highNotStarted = 0;
193
+ for (const sl of allSlices) {
194
+ if (sl.risk === "high") {
195
+ high++;
196
+ if (!sl.done && !sl.active) highNotStarted++;
197
+ } else if (sl.risk === "medium") {
198
+ med++;
199
+ } else {
200
+ low++;
201
+ }
202
+ }
203
+
204
+ let summary = ` Risk: ${low} low, ${med} med, ${high} high`;
205
+ if (highNotStarted > 0) {
206
+ summary += ` | ${th.fg("error", `${highNotStarted} high-risk not started`)}`;
207
+ }
208
+ lines.push(summary);
209
+
210
+ return lines;
211
+ }
212
+
213
+ // ─── Dependencies View ───────────────────────────────────────────────────────
214
+
215
+ export function renderDepsView(
216
+ data: VisualizerData,
217
+ th: Theme,
218
+ width: number,
219
+ ): string[] {
220
+ const lines: string[] = [];
221
+
222
+ // Milestone Dependencies
223
+ lines.push(th.fg("accent", th.bold("Milestone Dependencies")));
224
+ lines.push("");
225
+
226
+ const msDeps = data.milestones.filter((ms) => ms.dependsOn.length > 0);
227
+ if (msDeps.length === 0) {
228
+ lines.push(th.fg("dim", " No milestone dependencies."));
229
+ } else {
230
+ for (const ms of msDeps) {
231
+ for (const dep of ms.dependsOn) {
232
+ lines.push(
233
+ ` ${th.fg("text", dep)} ${th.fg("accent", "──►")} ${th.fg("text", ms.id)}`,
234
+ );
235
+ }
236
+ }
237
+ }
238
+
239
+ lines.push("");
240
+
241
+ // Slice Dependencies (active milestone)
242
+ lines.push(th.fg("accent", th.bold("Slice Dependencies (active milestone)")));
243
+ lines.push("");
244
+
245
+ const activeMs = data.milestones.find((ms) => ms.status === "active");
246
+ if (!activeMs) {
247
+ lines.push(th.fg("dim", " No active milestone."));
248
+ } else {
249
+ const slDeps = activeMs.slices.filter((sl) => sl.depends.length > 0);
250
+ if (slDeps.length === 0) {
251
+ lines.push(th.fg("dim", " No slice dependencies."));
252
+ } else {
253
+ for (const sl of slDeps) {
254
+ for (const dep of sl.depends) {
255
+ lines.push(
256
+ ` ${th.fg("text", dep)} ${th.fg("accent", "──►")} ${th.fg("text", sl.id)}`,
257
+ );
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ lines.push("");
264
+
265
+ // Critical Path section
266
+ lines.push(...renderCriticalPath(data, th, width));
267
+
268
+ return lines;
269
+ }
270
+
271
+ // ─── Critical Path ───────────────────────────────────────────────────────────
272
+
273
+ function renderCriticalPath(data: VisualizerData, th: Theme, _width: number): string[] {
274
+ const lines: string[] = [];
275
+ const cp = data.criticalPath;
276
+
277
+ lines.push(th.fg("accent", th.bold("Critical Path")));
278
+ lines.push("");
279
+
280
+ if (cp.milestonePath.length === 0) {
281
+ lines.push(th.fg("dim", " No critical path data."));
282
+ return lines;
283
+ }
284
+
285
+ // Milestone chain
286
+ const chain = cp.milestonePath.map(id => {
287
+ const ms = data.milestones.find(m => m.id === id);
288
+ const badge = th.fg("error", "[CRITICAL]");
289
+ return `${id} ${badge}`;
290
+ }).join(` ${th.fg("accent", "──►")} `);
291
+ lines.push(` ${chain}`);
292
+ lines.push("");
293
+
294
+ // Non-critical milestones with slack
295
+ for (const ms of data.milestones) {
296
+ if (cp.milestonePath.includes(ms.id)) continue;
297
+ const slack = cp.milestoneSlack.get(ms.id) ?? 0;
298
+ lines.push(th.fg("dim", ` ${ms.id} (slack: ${slack})`));
299
+ }
300
+
301
+ // Slice-level critical path
302
+ if (cp.slicePath.length > 0) {
303
+ lines.push("");
304
+ lines.push(th.fg("accent", th.bold("Slice Critical Path")));
305
+ lines.push("");
306
+
307
+ const sliceChain = cp.slicePath.join(` ${th.fg("accent", "──►")} `);
308
+ lines.push(` ${sliceChain}`);
309
+
310
+ // Bottleneck warnings
311
+ const activeMs = data.milestones.find(m => m.status === "active");
312
+ if (activeMs) {
313
+ for (const sid of cp.slicePath) {
314
+ const sl = activeMs.slices.find(s => s.id === sid);
315
+ if (sl && !sl.done && !sl.active) {
316
+ lines.push(th.fg("warning", ` ⚠ ${sid}: critical but not yet started`));
317
+ }
318
+ }
319
+ }
320
+ }
321
+
322
+ return lines;
323
+ }
324
+
325
+ // ─── Metrics View ────────────────────────────────────────────────────────────
326
+
327
+ export function renderMetricsView(
328
+ data: VisualizerData,
329
+ th: Theme,
330
+ width: number,
331
+ ): string[] {
332
+ const lines: string[] = [];
333
+
334
+ if (data.totals === null) {
335
+ lines.push(th.fg("dim", "No metrics data available."));
336
+ return lines;
337
+ }
338
+
339
+ const totals = data.totals;
340
+
341
+ // Summary line
342
+ lines.push(
343
+ th.fg("accent", th.bold("Summary")),
344
+ );
345
+ lines.push(
346
+ ` Cost: ${th.fg("text", formatCost(totals.cost))} ` +
347
+ `Tokens: ${th.fg("text", formatTokenCount(totals.tokens.total))} ` +
348
+ `Units: ${th.fg("text", String(totals.units))}`,
349
+ );
350
+ lines.push("");
351
+
352
+ const barWidth = Math.max(10, width - 40);
353
+
354
+ // By Phase
355
+ if (data.byPhase.length > 0) {
356
+ lines.push(th.fg("accent", th.bold("By Phase")));
357
+ lines.push("");
358
+
359
+ const maxPhaseCost = Math.max(...data.byPhase.map((p) => p.cost));
360
+
361
+ for (const phase of data.byPhase) {
362
+ const pct = totals.cost > 0 ? (phase.cost / totals.cost) * 100 : 0;
363
+ const fillLen =
364
+ maxPhaseCost > 0
365
+ ? Math.round((phase.cost / maxPhaseCost) * barWidth)
366
+ : 0;
367
+ const bar =
368
+ th.fg("accent", "█".repeat(fillLen)) +
369
+ th.fg("dim", "░".repeat(barWidth - fillLen));
370
+ const label = padRight(phase.phase, 14);
371
+ const costStr = formatCost(phase.cost);
372
+ const pctStr = `${pct.toFixed(1)}%`;
373
+ const tokenStr = formatTokenCount(phase.tokens.total);
374
+ lines.push(` ${label} ${bar} ${costStr} ${pctStr} ${tokenStr}`);
375
+ }
376
+
377
+ lines.push("");
378
+ }
379
+
380
+ // By Model
381
+ if (data.byModel.length > 0) {
382
+ lines.push(th.fg("accent", th.bold("By Model")));
383
+ lines.push("");
384
+
385
+ const maxModelCost = Math.max(...data.byModel.map((m) => m.cost));
386
+
387
+ for (const model of data.byModel) {
388
+ const pct = totals.cost > 0 ? (model.cost / totals.cost) * 100 : 0;
389
+ const fillLen =
390
+ maxModelCost > 0
391
+ ? Math.round((model.cost / maxModelCost) * barWidth)
392
+ : 0;
393
+ const bar =
394
+ th.fg("accent", "█".repeat(fillLen)) +
395
+ th.fg("dim", "░".repeat(barWidth - fillLen));
396
+ const label = padRight(model.model, 20);
397
+ const costStr = formatCost(model.cost);
398
+ const pctStr = `${pct.toFixed(1)}%`;
399
+ lines.push(` ${label} ${bar} ${costStr} ${pctStr}`);
400
+ }
401
+
402
+ lines.push("");
403
+ }
404
+
405
+ // Cost Projections
406
+ lines.push(...renderCostProjections(data, th, width));
407
+
408
+ return lines;
409
+ }
410
+
411
+ // ─── Cost Projections ────────────────────────────────────────────────────────
412
+
413
+ function renderCostProjections(data: VisualizerData, th: Theme, _width: number): string[] {
414
+ const lines: string[] = [];
415
+
416
+ if (!data.totals || data.bySlice.length === 0) return lines;
417
+
418
+ lines.push(th.fg("accent", th.bold("Projections")));
419
+ lines.push("");
420
+
421
+ // Average cost per slice
422
+ const sliceLevelEntries = data.bySlice.filter(s => s.sliceId.includes("/"));
423
+ if (sliceLevelEntries.length < 2) {
424
+ lines.push(th.fg("dim", " Insufficient data for projections (need 2+ completed slices)."));
425
+ return lines;
426
+ }
427
+
428
+ const totalSliceCost = sliceLevelEntries.reduce((sum, s) => sum + s.cost, 0);
429
+ const avgCostPerSlice = totalSliceCost / sliceLevelEntries.length;
430
+ const projectedRemaining = avgCostPerSlice * data.remainingSliceCount;
431
+
432
+ lines.push(` Avg cost/slice: ${th.fg("text", formatCost(avgCostPerSlice))}`);
433
+ lines.push(
434
+ ` Projected remaining: ${th.fg("text", formatCost(projectedRemaining))} ` +
435
+ `(${formatCost(avgCostPerSlice)}/slice × ${data.remainingSliceCount} remaining)`,
436
+ );
437
+
438
+ // Burn rate
439
+ if (data.totals.duration > 0) {
440
+ const costPerHour = data.totals.cost / (data.totals.duration / 3_600_000);
441
+ lines.push(` Burn rate: ${th.fg("text", formatCost(costPerHour) + "/hr")}`);
442
+ }
443
+
444
+ // Sparkline of per-slice costs
445
+ const sliceCosts = sliceLevelEntries.map(s => s.cost);
446
+ if (sliceCosts.length > 0) {
447
+ const spark = sparkline(sliceCosts);
448
+ lines.push(` Cost trend: ${spark}`);
449
+ }
450
+
451
+ // Budget warning: projected total > 2× current spend
452
+ const projectedTotal = data.totals.cost + projectedRemaining;
453
+ if (projectedTotal > 2 * data.totals.cost && data.remainingSliceCount > 0) {
454
+ lines.push(th.fg("warning", ` ⚠ Projected total ${formatCost(projectedTotal)} exceeds 2× current spend`));
455
+ }
456
+
457
+ return lines;
458
+ }
459
+
460
+ // ─── Timeline View (Gantt) ──────────────────────────────────────────────────
461
+
462
+ export function renderTimelineView(
463
+ data: VisualizerData,
464
+ th: Theme,
465
+ width: number,
466
+ ): string[] {
467
+ const lines: string[] = [];
468
+
469
+ if (data.units.length === 0) {
470
+ lines.push(th.fg("dim", "No execution history."));
471
+ return lines;
472
+ }
473
+
474
+ // Gantt mode for wide terminals, list mode for narrow
475
+ if (width >= 90) {
476
+ return renderGanttView(data, th, width);
477
+ }
478
+
479
+ return renderTimelineList(data, th, width);
480
+ }
481
+
482
+ function renderTimelineList(data: VisualizerData, th: Theme, width: number): string[] {
483
+ const lines: string[] = [];
484
+
485
+ // Show up to 20 most recent (units are sorted by startedAt asc, show most recent)
486
+ const recent = data.units.slice(-20).reverse();
487
+
488
+ const maxDuration = Math.max(
489
+ ...recent.map((u) => u.finishedAt - u.startedAt),
490
+ );
491
+ const timeBarWidth = Math.max(4, Math.min(12, width - 60));
492
+
493
+ for (const unit of recent) {
494
+ const dt = new Date(unit.startedAt);
495
+ const hh = String(dt.getHours()).padStart(2, "0");
496
+ const mm = String(dt.getMinutes()).padStart(2, "0");
497
+ const time = `${hh}:${mm}`;
498
+
499
+ const duration = unit.finishedAt - unit.startedAt;
500
+ const glyph =
501
+ unit.finishedAt > 0
502
+ ? th.fg("success", "✓")
503
+ : th.fg("accent", "▸");
504
+
505
+ const typeLabel = padRight(unit.type, 16);
506
+ const idLabel = padRight(unit.id, 14);
507
+
508
+ const fillLen =
509
+ maxDuration > 0
510
+ ? Math.round((duration / maxDuration) * timeBarWidth)
511
+ : 0;
512
+ const bar =
513
+ th.fg("accent", "█".repeat(fillLen)) +
514
+ th.fg("dim", "░".repeat(timeBarWidth - fillLen));
515
+
516
+ const durStr = formatDuration(duration);
517
+ const costStr = formatCost(unit.cost);
518
+
519
+ const line = ` ${time} ${glyph} ${typeLabel} ${idLabel} ${bar} ${durStr} ${costStr}`;
520
+ lines.push(truncateToWidth(line, width));
521
+ }
522
+
523
+ return lines;
524
+ }
525
+
526
+ function renderGanttView(data: VisualizerData, th: Theme, width: number): string[] {
527
+ const lines: string[] = [];
528
+ const recent = data.units.slice(-20);
529
+ if (recent.length === 0) return lines;
530
+
531
+ const finishedUnits = recent.filter(u => u.finishedAt > 0);
532
+ if (finishedUnits.length === 0) return renderTimelineList(data, th, width);
533
+
534
+ const minStart = Math.min(...recent.map(u => u.startedAt));
535
+ const maxEnd = Math.max(...recent.map(u => u.finishedAt > 0 ? u.finishedAt : Date.now()));
536
+ const totalSpan = maxEnd - minStart;
537
+ if (totalSpan <= 0) return renderTimelineList(data, th, width);
538
+
539
+ const gutterWidth = 20;
540
+ const barArea = Math.max(10, width - gutterWidth - 25);
541
+
542
+ // Time axis labels
543
+ const startLabel = formatTimeLabel(minStart);
544
+ const endLabel = formatTimeLabel(maxEnd);
545
+ lines.push(
546
+ `${" ".repeat(gutterWidth)} ${th.fg("dim", startLabel)}` +
547
+ `${" ".repeat(Math.max(1, barArea - startLabel.length - endLabel.length))}` +
548
+ `${th.fg("dim", endLabel)}`,
549
+ );
550
+
551
+ // Phase tracking for separators
552
+ let lastPhase = "";
553
+
554
+ for (const unit of recent) {
555
+ const phase = classifyUnitPhase(unit.type);
556
+ if (phase !== lastPhase && lastPhase !== "") {
557
+ lines.push(th.fg("dim", " " + "─".repeat(width - 4)));
558
+ }
559
+ lastPhase = phase;
560
+
561
+ const end = unit.finishedAt > 0 ? unit.finishedAt : Date.now();
562
+ const startPos = Math.round(((unit.startedAt - minStart) / totalSpan) * barArea);
563
+ const endPos = Math.round(((end - minStart) / totalSpan) * barArea);
564
+ const barLen = Math.max(1, endPos - startPos);
565
+
566
+ const phaseColor =
567
+ phase === "research" ? "dim" :
568
+ phase === "planning" ? "accent" :
569
+ phase === "execution" ? "success" :
570
+ "warning";
571
+
572
+ const barStr =
573
+ " ".repeat(startPos) +
574
+ th.fg(phaseColor, "█".repeat(barLen)) +
575
+ " ".repeat(Math.max(0, barArea - startPos - barLen));
576
+
577
+ const gutter = padRight(
578
+ truncateToWidth(`${unit.type.slice(0, 8)} ${unit.id}`, gutterWidth - 1),
579
+ gutterWidth,
580
+ );
581
+
582
+ const duration = end - unit.startedAt;
583
+ const durStr = formatDuration(duration);
584
+ const costStr = formatCost(unit.cost);
585
+
586
+ lines.push(truncateToWidth(`${gutter}${barStr} ${durStr} ${costStr}`, width));
587
+ }
588
+
589
+ return lines;
590
+ }
591
+
592
+ function formatTimeLabel(ts: number): string {
593
+ const dt = new Date(ts);
594
+ return `${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`;
595
+ }
596
+
597
+ // ─── Agent View ──────────────────────────────────────────────────────────────
598
+
599
+ export function renderAgentView(
600
+ data: VisualizerData,
601
+ th: Theme,
602
+ width: number,
603
+ ): string[] {
604
+ const lines: string[] = [];
605
+ const activity = data.agentActivity;
606
+
607
+ if (!activity) {
608
+ lines.push(th.fg("dim", "No agent activity data."));
609
+ return lines;
610
+ }
611
+
612
+ // Status line
613
+ const statusDot = activity.active
614
+ ? th.fg("success", "●")
615
+ : th.fg("dim", "○");
616
+ const statusText = activity.active ? "ACTIVE" : "IDLE";
617
+ const elapsedStr = activity.active ? formatDuration(activity.elapsed) : "—";
618
+
619
+ lines.push(
620
+ joinColumns(
621
+ `Status: ${statusDot} ${statusText}`,
622
+ `Elapsed: ${elapsedStr}`,
623
+ width,
624
+ ),
625
+ );
626
+
627
+ if (activity.currentUnit) {
628
+ lines.push(`Current: ${th.fg("accent", `${activity.currentUnit.type} ${activity.currentUnit.id}`)}`);
629
+ } else {
630
+ lines.push(th.fg("dim", "Not in auto mode"));
631
+ }
632
+
633
+ lines.push("");
634
+
635
+ // Progress bar
636
+ const completed = activity.completedUnits;
637
+ const total = Math.max(completed, activity.totalSlices);
638
+ if (total > 0) {
639
+ const pct = Math.min(1, completed / total);
640
+ const barW = Math.max(10, Math.min(30, width - 30));
641
+ const fillLen = Math.round(pct * barW);
642
+ const bar =
643
+ th.fg("accent", "█".repeat(fillLen)) +
644
+ th.fg("dim", "░".repeat(barW - fillLen));
645
+ lines.push(`Progress ${bar} ${completed}/${total} slices`);
646
+ }
647
+
648
+ // Rate and session stats
649
+ const rateStr = activity.completionRate > 0
650
+ ? `${activity.completionRate.toFixed(1)} units/hr`
651
+ : "—";
652
+ lines.push(
653
+ `Rate: ${th.fg("text", rateStr)} ` +
654
+ `Session: ${th.fg("text", formatCost(activity.sessionCost))} ` +
655
+ `${th.fg("text", formatTokenCount(activity.sessionTokens))} tokens`,
656
+ );
657
+
658
+ lines.push("");
659
+
660
+ // Recent completed units (last 5)
661
+ const recentUnits = data.units.filter(u => u.finishedAt > 0).slice(-5).reverse();
662
+ if (recentUnits.length > 0) {
663
+ lines.push(th.fg("accent", th.bold("Recent (last 5):")));
664
+ for (const u of recentUnits) {
665
+ const dt = new Date(u.startedAt);
666
+ const hh = String(dt.getHours()).padStart(2, "0");
667
+ const mm = String(dt.getMinutes()).padStart(2, "0");
668
+ const dur = formatDuration(u.finishedAt - u.startedAt);
669
+ const cost = formatCost(u.cost);
670
+ const typeLabel = padRight(u.type, 16);
671
+ lines.push(
672
+ truncateToWidth(
673
+ ` ${hh}:${mm} ${th.fg("success", "✓")} ${typeLabel} ${padRight(u.id, 16)} ${dur} ${cost}`,
674
+ width,
675
+ ),
676
+ );
677
+ }
678
+ } else {
679
+ lines.push(th.fg("dim", "No completed units yet."));
680
+ }
681
+
682
+ return lines;
683
+ }
684
+
685
+ // ─── Changelog View ──────────────────────────────────────────────────────────
686
+
687
+ export function renderChangelogView(
688
+ data: VisualizerData,
689
+ th: Theme,
690
+ width: number,
691
+ ): string[] {
692
+ const lines: string[] = [];
693
+ const changelog = data.changelog;
694
+
695
+ if (changelog.entries.length === 0) {
696
+ lines.push(th.fg("dim", "No completed slices yet."));
697
+ return lines;
698
+ }
699
+
700
+ lines.push(th.fg("accent", th.bold("Changes")));
701
+ lines.push("");
702
+
703
+ for (const entry of changelog.entries) {
704
+ const header = `${entry.milestoneId}/${entry.sliceId}: ${entry.title}`;
705
+ lines.push(th.fg("success", header));
706
+
707
+ if (entry.oneLiner) {
708
+ lines.push(` "${th.fg("text", entry.oneLiner)}"`);
709
+ }
710
+
711
+ if (entry.filesModified.length > 0) {
712
+ lines.push(" Files:");
713
+ for (const f of entry.filesModified) {
714
+ lines.push(
715
+ truncateToWidth(
716
+ ` ${th.fg("success", "✓")} ${f.path} — ${f.description}`,
717
+ width,
718
+ ),
719
+ );
720
+ }
721
+ }
722
+
723
+ if (entry.completedAt) {
724
+ lines.push(th.fg("dim", ` Completed: ${entry.completedAt}`));
725
+ }
726
+
727
+ lines.push("");
728
+ }
729
+
730
+ return lines;
731
+ }
732
+
733
+ // ─── Export View ─────────────────────────────────────────────────────────────
734
+
735
+ export function renderExportView(
736
+ _data: VisualizerData,
737
+ th: Theme,
738
+ _width: number,
739
+ lastExportPath?: string,
740
+ ): string[] {
741
+ const lines: string[] = [];
742
+
743
+ lines.push(th.fg("accent", th.bold("Export Options")));
744
+ lines.push("");
745
+ lines.push(` ${th.fg("accent", "[m]")} Markdown report — full project summary with tables`);
746
+ lines.push(` ${th.fg("accent", "[j]")} JSON report — machine-readable project data`);
747
+ lines.push(` ${th.fg("accent", "[s]")} Snapshot — current view as plain text`);
748
+
749
+ if (lastExportPath) {
750
+ lines.push("");
751
+ lines.push(th.fg("dim", `Last export: ${lastExportPath}`));
752
+ }
753
+
754
+ return lines;
755
+ }