gsd-pi 2.19.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 (249) 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-prompts.ts +103 -24
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
  9. package/dist/resources/extensions/gsd/auto.ts +424 -30
  10. package/dist/resources/extensions/gsd/commands.ts +518 -36
  11. package/dist/resources/extensions/gsd/context-budget.ts +243 -0
  12. package/dist/resources/extensions/gsd/context-store.ts +195 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
  14. package/dist/resources/extensions/gsd/db-writer.ts +341 -0
  15. package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
  16. package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
  17. package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  18. package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
  19. package/dist/resources/extensions/gsd/doctor.ts +283 -2
  20. package/dist/resources/extensions/gsd/export.ts +81 -2
  21. package/dist/resources/extensions/gsd/files.ts +39 -9
  22. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  23. package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
  24. package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
  25. package/dist/resources/extensions/gsd/history.ts +0 -1
  26. package/dist/resources/extensions/gsd/index.ts +277 -1
  27. package/dist/resources/extensions/gsd/md-importer.ts +526 -0
  28. package/dist/resources/extensions/gsd/metrics.ts +39 -3
  29. package/dist/resources/extensions/gsd/notifications.ts +0 -1
  30. package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
  31. package/dist/resources/extensions/gsd/preferences.ts +125 -150
  32. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
  33. package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  34. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  35. package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
  36. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  37. package/dist/resources/extensions/gsd/quick.ts +156 -0
  38. package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
  39. package/dist/resources/extensions/gsd/skill-health.ts +417 -0
  40. package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
  41. package/dist/resources/extensions/gsd/state.ts +30 -0
  42. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  43. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  44. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  45. package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  46. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  47. package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  48. package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  49. package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  50. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  51. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  52. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  53. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  54. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  55. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  56. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  57. package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  58. package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  59. package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  60. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  61. package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  62. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  63. package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  64. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  65. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  66. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  67. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  68. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  69. package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  70. package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  71. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
  72. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  73. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  74. package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  75. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  76. package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  77. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  78. package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  79. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
  80. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  81. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
  82. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  83. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  84. package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  85. package/dist/resources/extensions/gsd/types.ts +29 -0
  86. package/dist/resources/extensions/gsd/undo.ts +0 -1
  87. package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
  88. package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
  89. package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
  90. package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
  91. package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
  92. package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
  93. package/dist/resources/extensions/remote-questions/config.ts +4 -2
  94. package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
  95. package/dist/resources/extensions/remote-questions/format.ts +154 -8
  96. package/dist/resources/extensions/remote-questions/manager.ts +9 -7
  97. package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
  98. package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  99. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  100. package/dist/resources/extensions/remote-questions/types.ts +2 -1
  101. package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  102. package/dist/resources/extensions/voice/index.ts +4 -3
  103. package/package.json +1 -1
  104. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
  106. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  109. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
  113. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
  115. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
  117. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
  119. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
  120. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
  122. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
  124. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
  126. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  128. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
  130. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
  133. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
  136. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  138. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  140. package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
  141. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  142. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
  143. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  144. package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
  145. package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
  146. package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
  147. package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
  148. package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
  149. package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
  150. package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
  151. package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
  152. package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
  153. package/src/resources/extensions/google-search/index.ts +164 -47
  154. package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
  155. package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
  156. package/src/resources/extensions/gsd/auto.ts +424 -30
  157. package/src/resources/extensions/gsd/commands.ts +518 -36
  158. package/src/resources/extensions/gsd/context-budget.ts +243 -0
  159. package/src/resources/extensions/gsd/context-store.ts +195 -0
  160. package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
  161. package/src/resources/extensions/gsd/db-writer.ts +341 -0
  162. package/src/resources/extensions/gsd/debug-logger.ts +178 -0
  163. package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
  164. package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  165. package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
  166. package/src/resources/extensions/gsd/doctor.ts +283 -2
  167. package/src/resources/extensions/gsd/export.ts +81 -2
  168. package/src/resources/extensions/gsd/files.ts +39 -9
  169. package/src/resources/extensions/gsd/git-service.ts +6 -0
  170. package/src/resources/extensions/gsd/gsd-db.ts +752 -0
  171. package/src/resources/extensions/gsd/guided-flow.ts +26 -1
  172. package/src/resources/extensions/gsd/history.ts +0 -1
  173. package/src/resources/extensions/gsd/index.ts +277 -1
  174. package/src/resources/extensions/gsd/md-importer.ts +526 -0
  175. package/src/resources/extensions/gsd/metrics.ts +39 -3
  176. package/src/resources/extensions/gsd/notifications.ts +0 -1
  177. package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
  178. package/src/resources/extensions/gsd/preferences.ts +125 -150
  179. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
  180. package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  181. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  182. package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
  183. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  184. package/src/resources/extensions/gsd/quick.ts +156 -0
  185. package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
  186. package/src/resources/extensions/gsd/skill-health.ts +417 -0
  187. package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
  188. package/src/resources/extensions/gsd/state.ts +30 -0
  189. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  190. package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  191. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  193. package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  194. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  195. package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  196. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  197. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  198. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  199. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  200. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  201. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  202. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  203. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  204. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  205. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  206. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  207. package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  208. package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  209. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  210. package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  211. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  212. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  213. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  214. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  215. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  216. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  217. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  218. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
  219. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  220. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  221. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  222. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  223. package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  224. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  225. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  226. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
  227. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  228. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
  229. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  230. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  231. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  232. package/src/resources/extensions/gsd/types.ts +29 -0
  233. package/src/resources/extensions/gsd/undo.ts +0 -1
  234. package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
  235. package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
  236. package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
  237. package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
  238. package/src/resources/extensions/gsd/worktree-command.ts +18 -0
  239. package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
  240. package/src/resources/extensions/remote-questions/config.ts +4 -2
  241. package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
  242. package/src/resources/extensions/remote-questions/format.ts +154 -8
  243. package/src/resources/extensions/remote-questions/manager.ts +9 -7
  244. package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
  245. package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  246. package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  247. package/src/resources/extensions/remote-questions/types.ts +2 -1
  248. package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  249. package/src/resources/extensions/voice/index.ts +4 -3
@@ -3,7 +3,7 @@
3
3
  import type { Theme } from "@gsd/pi-coding-agent";
4
4
  import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
5
5
  import type { VisualizerData, VisualizerMilestone } from "./visualizer-data.js";
6
- import { formatCost, formatTokenCount } from "./metrics.js";
6
+ import { formatCost, formatTokenCount, classifyUnitPhase } from "./metrics.js";
7
7
 
8
8
  // ─── Local Helpers ───────────────────────────────────────────────────────────
9
9
 
@@ -32,16 +32,46 @@ function joinColumns(left: string, right: string, width: number): string {
32
32
  return left + " ".repeat(width - leftW - rightW) + right;
33
33
  }
34
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
+
35
43
  // ─── Progress View ───────────────────────────────────────────────────────────
36
44
 
45
+ export interface ProgressFilter {
46
+ text: string;
47
+ field: "all" | "status" | "risk" | "keyword";
48
+ }
49
+
37
50
  export function renderProgressView(
38
51
  data: VisualizerData,
39
52
  th: Theme,
40
53
  width: number,
54
+ filter?: ProgressFilter,
41
55
  ): string[] {
42
56
  const lines: string[] = [];
43
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
+
44
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
+
45
75
  // Milestone header line
46
76
  const statusGlyph =
47
77
  ms.status === "complete"
@@ -70,6 +100,11 @@ export function renderProgressView(
70
100
  }
71
101
 
72
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
+
73
108
  // Slice line
74
109
  const slGlyph = sl.done
75
110
  ? th.fg("success", "✓")
@@ -103,6 +138,78 @@ export function renderProgressView(
103
138
  return lines;
104
139
  }
105
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
+
106
213
  // ─── Dependencies View ───────────────────────────────────────────────────────
107
214
 
108
215
  export function renderDepsView(
@@ -153,6 +260,65 @@ export function renderDepsView(
153
260
  }
154
261
  }
155
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
+
156
322
  return lines;
157
323
  }
158
324
 
@@ -232,12 +398,66 @@ export function renderMetricsView(
232
398
  const pctStr = `${pct.toFixed(1)}%`;
233
399
  lines.push(` ${label} ${bar} ${costStr} ${pctStr}`);
234
400
  }
401
+
402
+ lines.push("");
235
403
  }
236
404
 
405
+ // Cost Projections
406
+ lines.push(...renderCostProjections(data, th, width));
407
+
237
408
  return lines;
238
409
  }
239
410
 
240
- // ─── Timeline View ──────────────────────────────────────────────────────────
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) ──────────────────────────────────────────────────
241
461
 
242
462
  export function renderTimelineView(
243
463
  data: VisualizerData,
@@ -251,6 +471,17 @@ export function renderTimelineView(
251
471
  return lines;
252
472
  }
253
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
+
254
485
  // Show up to 20 most recent (units are sorted by startedAt asc, show most recent)
255
486
  const recent = data.units.slice(-20).reverse();
256
487
 
@@ -291,3 +522,234 @@ export function renderTimelineView(
291
522
 
292
523
  return lines;
293
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
+ }
@@ -13,6 +13,7 @@
13
13
  import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
14
14
  import { loadPrompt } from "./prompt-loader.js";
15
15
  import { autoCommitCurrentBranch } from "./worktree.js";
16
+ import { runWorktreePostCreateHook } from "./auto-worktree.js";
16
17
  import { showConfirm } from "../shared/confirm-ui.js";
17
18
  import { gsdRoot, milestonesDir } from "./paths.js";
18
19
  import {
@@ -360,6 +361,12 @@ async function handleCreate(
360
361
  const mainBase = originalCwd ?? basePath;
361
362
  const info = createWorktree(mainBase, name);
362
363
 
364
+ // Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
365
+ const hookError = runWorktreePostCreateHook(mainBase, info.path);
366
+ if (hookError) {
367
+ ctx.ui.notify(hookError, "warning");
368
+ }
369
+
363
370
  // Track original cwd before switching
364
371
  if (!originalCwd) originalCwd = basePath;
365
372
 
@@ -672,6 +679,17 @@ async function handleMerge(
672
679
  // Try a direct squash-merge first. Only fall back to LLM on conflict.
673
680
  const commitType = inferCommitType(name);
674
681
  const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
682
+
683
+ // Reconcile worktree DB into main DB before squash merge
684
+ const wtDbPath = join(worktreePath(basePath, name), ".gsd", "gsd.db");
685
+ const mainDbPath = join(basePath, ".gsd", "gsd.db");
686
+ if (existsSync(wtDbPath) && existsSync(mainDbPath)) {
687
+ try {
688
+ const { reconcileWorktreeDb } = await import("./gsd-db.js");
689
+ reconcileWorktreeDb(mainDbPath, wtDbPath);
690
+ } catch { /* non-fatal */ }
691
+ }
692
+
675
693
  try {
676
694
  mergeWorktreeToMain(basePath, name, commitMessage);
677
695
  ctx.ui.notify(
@@ -94,7 +94,7 @@ export function worktreeBranchName(name: string): string {
94
94
  *
95
95
  * @param opts.branch — override the default `worktree/<name>` branch name
96
96
  */
97
- export function createWorktree(basePath: string, name: string, opts: { branch?: string; startPoint?: string } = {}): WorktreeInfo {
97
+ export function createWorktree(basePath: string, name: string, opts: { branch?: string; startPoint?: string; reuseExistingBranch?: boolean } = {}): WorktreeInfo {
98
98
  // Validate name: alphanumeric, hyphens, underscores only
99
99
  if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
100
100
  throw new Error(`Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
@@ -133,9 +133,16 @@ export function createWorktree(basePath: string, name: string, opts: { branch?:
133
133
  );
134
134
  }
135
135
 
136
- // Reset the stale branch to the start point, then attach worktree to it
137
- nativeBranchForceReset(basePath, branch, startPoint);
138
- nativeWorktreeAdd(basePath, wtPath, branch);
136
+ if (opts.reuseExistingBranch) {
137
+ // Attach worktree to the existing branch as-is (preserving commits).
138
+ // Used when resuming auto-mode: the milestone branch has valid work
139
+ // from prior sessions that must not be reset.
140
+ nativeWorktreeAdd(basePath, wtPath, branch);
141
+ } else {
142
+ // Reset the stale branch to the start point, then attach worktree to it
143
+ nativeBranchForceReset(basePath, branch, startPoint);
144
+ nativeWorktreeAdd(basePath, wtPath, branch);
145
+ }
139
146
  } else {
140
147
  nativeWorktreeAdd(basePath, wtPath, branch, true, startPoint);
141
148
  }
@@ -16,12 +16,14 @@ export interface ResolvedConfig {
16
16
  const ENV_KEYS: Record<RemoteChannel, string> = {
17
17
  slack: "SLACK_BOT_TOKEN",
18
18
  discord: "DISCORD_BOT_TOKEN",
19
+ telegram: "TELEGRAM_BOT_TOKEN",
19
20
  };
20
21
 
21
22
  // Channel ID format validation — prevents SSRF if preferences are attacker-controlled
22
23
  const CHANNEL_ID_PATTERNS: Record<RemoteChannel, RegExp> = {
23
24
  slack: /^[A-Z0-9]{9,12}$/,
24
25
  discord: /^\d{17,20}$/,
26
+ telegram: /^-?\d{5,20}$/,
25
27
  };
26
28
 
27
29
  const DEFAULT_TIMEOUT_MINUTES = 5;
@@ -35,7 +37,7 @@ export function resolveRemoteConfig(): ResolvedConfig | null {
35
37
  const prefs = loadEffectiveGSDPreferences();
36
38
  const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
37
39
  if (!rq || !rq.channel || !rq.channel_id) return null;
38
- if (rq.channel !== "slack" && rq.channel !== "discord") return null;
40
+ if (rq.channel !== "slack" && rq.channel !== "discord" && rq.channel !== "telegram") return null;
39
41
 
40
42
  const channelId = String(rq.channel_id);
41
43
  if (!CHANNEL_ID_PATTERNS[rq.channel].test(channelId)) return null;
@@ -59,7 +61,7 @@ export function getRemoteConfigStatus(): string {
59
61
  const prefs = loadEffectiveGSDPreferences();
60
62
  const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
61
63
  if (!rq || !rq.channel || !rq.channel_id) return "Remote questions: not configured";
62
- if (rq.channel !== "slack" && rq.channel !== "discord") return `Remote questions: unknown channel type \"${rq.channel}\"`;
64
+ if (rq.channel !== "slack" && rq.channel !== "discord" && rq.channel !== "telegram") return `Remote questions: unknown channel type \"${rq.channel}\"`;
63
65
  const channelId = String(rq.channel_id);
64
66
  if (!CHANNEL_ID_PATTERNS[rq.channel].test(channelId)) return `Remote questions: invalid ${rq.channel} channel ID format`;
65
67
  const envVar = ENV_KEYS[rq.channel];
@@ -3,12 +3,10 @@
3
3
  */
4
4
 
5
5
  import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
6
- import { formatForDiscord, parseDiscordResponse } from "./format.js";
6
+ import { formatForDiscord, parseDiscordResponse, DISCORD_NUMBER_EMOJIS } from "./format.js";
7
7
 
8
8
  const DISCORD_API = "https://discord.com/api/v10";
9
9
  const PER_REQUEST_TIMEOUT_MS = 15_000;
10
- const NUMBER_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"];
11
-
12
10
  export class DiscordAdapter implements ChannelAdapter {
13
11
  readonly name = "discord" as const;
14
12
  private botUserId: string | null = null;
@@ -102,7 +100,7 @@ export class DiscordAdapter implements ChannelAdapter {
102
100
 
103
101
  private async checkReactions(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null> {
104
102
  const reactions: Array<{ emoji: string; count: number }> = [];
105
- for (const emoji of NUMBER_EMOJIS) {
103
+ for (const emoji of DISCORD_NUMBER_EMOJIS) {
106
104
  try {
107
105
  const users = await this.discordApi("GET", `/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent(emoji)}`);
108
106
  if (Array.isArray(users)) {