@rse/ase 1.0.7 → 2.0.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 (279) hide show
  1. package/dst/ase-agent-definition-api.d.ts +184 -0
  2. package/dst/ase-agent-definition-api.js +6 -0
  3. package/dst/ase-agent-definition-builtin/condenser.js +42 -0
  4. package/dst/ase-agent-definition-builtin/editor.js +45 -0
  5. package/dst/ase-agent-definition-builtin/estimator.js +98 -0
  6. package/dst/ase-agent-definition-builtin/implementer.js +121 -0
  7. package/dst/ase-agent-definition-builtin/integrator.js +56 -0
  8. package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
  9. package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
  10. package/dst/ase-agent-definition-builtin/researcher.js +77 -0
  11. package/dst/ase-agent-definition-core.js +827 -0
  12. package/dst/ase-agent-fleet.js +772 -0
  13. package/dst/ase-agent-process.js +232 -0
  14. package/dst/ase-agent-relay-client.js +509 -0
  15. package/dst/ase-agent-relay-core.js +213 -0
  16. package/dst/ase-agent-relay-server-bind.js +242 -0
  17. package/dst/ase-agent-relay-server-cli.js +50 -0
  18. package/dst/ase-agent-runtime-claim.js +189 -0
  19. package/dst/ase-agent-runtime-engine.js +1100 -0
  20. package/dst/ase-agent-runtime-exec.js +275 -0
  21. package/dst/ase-agent-runtime-phase.js +581 -0
  22. package/dst/ase-agent-runtime-request.js +272 -0
  23. package/dst/ase-agent-runtime-tool.js +207 -0
  24. package/dst/ase-agent-runtime.js +1316 -0
  25. package/dst/ase-agent.js +65 -0
  26. package/dst/ase-artifact.js +6 -5
  27. package/dst/ase-config-core.js +465 -0
  28. package/dst/ase-config-schema.js +351 -0
  29. package/dst/ase-config-scope.js +172 -0
  30. package/dst/ase-config.js +25 -585
  31. package/dst/ase-hook.js +228 -111
  32. package/dst/ase-lib-daemon-cli.js +130 -0
  33. package/dst/ase-lib-hash.js +20 -0
  34. package/dst/{ase-log.js → ase-lib-log.js} +40 -7
  35. package/dst/ase-lib-server-bind.js +115 -0
  36. package/dst/ase-lib-server-cli.js +474 -0
  37. package/dst/{ase-table.js → ase-lib-table.js} +1 -1
  38. package/dst/{ase-version.js → ase-lib-version.js} +3 -0
  39. package/dst/ase-man.js +96 -0
  40. package/dst/ase-mcp.js +166 -13
  41. package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
  42. package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
  43. package/dst/ase-service-otel.js +1246 -0
  44. package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
  45. package/dst/ase-service-state.js +101 -0
  46. package/dst/ase-service.js +251 -76
  47. package/dst/ase-setup-common.js +141 -0
  48. package/dst/ase-setup-mcp.js +359 -0
  49. package/dst/ase-setup-settings.js +837 -0
  50. package/dst/ase-setup.js +113 -869
  51. package/dst/ase-spec.js +31 -19
  52. package/dst/ase-statusline.js +108 -43
  53. package/dst/ase-task-board-core.js +739 -0
  54. package/dst/ase-task-board-filter.js +127 -0
  55. package/dst/ase-task-board-flow.js +237 -0
  56. package/dst/ase-task-board-graph.js +497 -0
  57. package/dst/ase-task-board-stats.js +263 -0
  58. package/dst/ase-task-board-tui-control.js +775 -0
  59. package/dst/ase-task-board-tui-model.js +1008 -0
  60. package/dst/ase-task-board-tui-popup.js +626 -0
  61. package/dst/ase-task-board-tui-style.js +106 -0
  62. package/dst/ase-task-board-tui-view.js +847 -0
  63. package/dst/ase-task-board-tui.js +189 -0
  64. package/dst/ase-task-board-web-client.css +1 -0
  65. package/dst/ase-task-board-web-client.html +20 -0
  66. package/dst/ase-task-board-web-client.js +81 -0
  67. package/dst/ase-task-board-web-server.js +823 -0
  68. package/dst/ase-task-board.js +134 -0
  69. package/dst/ase-task-format.js +757 -0
  70. package/dst/ase-task-request.js +208 -0
  71. package/dst/ase-task-store-client.js +495 -0
  72. package/dst/ase-task-store-core.js +804 -0
  73. package/dst/ase-task-store-issues.js +237 -0
  74. package/dst/ase-task-store-plugin-api.js +6 -0
  75. package/dst/ase-task-store-plugin-ase.js +484 -0
  76. package/dst/ase-task-store-plugin-delegate.js +137 -0
  77. package/dst/ase-task-store-plugin-gitea.js +666 -0
  78. package/dst/ase-task-store-plugin-github.js +696 -0
  79. package/dst/ase-task-store-plugin-gitlab.js +668 -0
  80. package/dst/ase-task-store-server-bind.js +538 -0
  81. package/dst/ase-task-store-server-cli.js +154 -0
  82. package/dst/ase-task.js +1335 -357
  83. package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
  84. package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
  85. package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
  86. package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
  87. package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
  88. package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
  89. package/dst/ase-util.js +20 -0
  90. package/dst/ase.js +108 -32
  91. package/dst/man/ase-agent-adhoc.1 +515 -0
  92. package/dst/man/ase-agent-condenser.1 +193 -0
  93. package/dst/man/ase-agent-editor.1 +212 -0
  94. package/dst/man/ase-agent-estimator.1 +214 -0
  95. package/dst/man/ase-agent-fleet.1 +185 -0
  96. package/dst/man/ase-agent-implementer.1 +233 -0
  97. package/dst/man/ase-agent-integrator.1 +213 -0
  98. package/dst/man/ase-agent-qualifier.1 +214 -0
  99. package/dst/man/ase-agent-rebooter.1 +193 -0
  100. package/dst/man/ase-agent-relay.1 +128 -0
  101. package/dst/man/ase-agent-researcher.1 +217 -0
  102. package/dst/man/ase-agent.1 +264 -0
  103. package/dst/man/ase-artifact-list.1 +38 -0
  104. package/dst/man/ase-artifact-name.1 +39 -0
  105. package/dst/man/ase-artifact.1 +56 -0
  106. package/dst/man/ase-config-delete.1 +45 -0
  107. package/dst/man/ase-config-edit.1 +48 -0
  108. package/dst/man/ase-config-get.1 +46 -0
  109. package/dst/man/ase-config-init.1 +60 -0
  110. package/dst/man/ase-config-list.1 +42 -0
  111. package/dst/man/ase-config-set.1 +52 -0
  112. package/dst/man/ase-config.1 +225 -0
  113. package/dst/man/ase-hook-permission-request.1 +36 -0
  114. package/dst/man/ase-hook-pre-tool-use.1 +59 -0
  115. package/dst/man/ase-hook-session-end.1 +47 -0
  116. package/dst/man/ase-hook-session-persona.1 +37 -0
  117. package/dst/man/ase-hook-session-start.1 +81 -0
  118. package/dst/man/ase-hook-stop.1 +34 -0
  119. package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
  120. package/dst/man/ase-hook.1 +51 -0
  121. package/dst/man/ase-man.1 +67 -0
  122. package/dst/man/ase-mcp.1 +61 -0
  123. package/dst/man/ase-service-log.1 +43 -0
  124. package/dst/man/ase-service-send.1 +47 -0
  125. package/dst/man/ase-service-start.1 +54 -0
  126. package/dst/man/ase-service-status.1 +46 -0
  127. package/dst/man/ase-service-stop.1 +37 -0
  128. package/dst/man/ase-service.1 +85 -0
  129. package/dst/man/ase-setup-disable.1 +44 -0
  130. package/dst/man/ase-setup-enable.1 +45 -0
  131. package/dst/man/ase-setup-install.1 +73 -0
  132. package/dst/man/ase-setup-mcp.1 +124 -0
  133. package/dst/man/ase-setup-status.1 +60 -0
  134. package/dst/man/ase-setup-statusline.1 +92 -0
  135. package/dst/man/ase-setup-telemetry.1 +92 -0
  136. package/dst/man/ase-setup-uninstall.1 +59 -0
  137. package/dst/man/ase-setup-update.1 +64 -0
  138. package/dst/man/ase-setup.1 +93 -0
  139. package/dst/man/ase-spec-export.1 +62 -0
  140. package/dst/man/ase-spec-lint.1 +40 -0
  141. package/dst/man/ase-spec-preview.1 +40 -0
  142. package/dst/man/ase-spec.1 +46 -0
  143. package/dst/man/ase-statusline.1 +189 -0
  144. package/dst/man/ase-task-assign.1 +71 -0
  145. package/dst/man/ase-task-board.1 +384 -0
  146. package/dst/man/ase-task-delete.1 +35 -0
  147. package/dst/man/ase-task-edit.1 +46 -0
  148. package/dst/man/ase-task-idscheme.1 +65 -0
  149. package/dst/man/ase-task-lifecycle.1 +57 -0
  150. package/dst/man/ase-task-list.1 +47 -0
  151. package/dst/man/ase-task-load.1 +36 -0
  152. package/dst/man/ase-task-newid.1 +61 -0
  153. package/dst/man/ase-task-purge.1 +37 -0
  154. package/dst/man/ase-task-rename.1 +44 -0
  155. package/dst/man/ase-task-request.1 +61 -0
  156. package/dst/man/ase-task-save.1 +48 -0
  157. package/dst/man/ase-task-status.1 +55 -0
  158. package/dst/man/ase-task-store.1 +194 -0
  159. package/dst/man/ase-task-view.1 +40 -0
  160. package/dst/man/ase-task.1 +160 -0
  161. package/dst/man/ase-util-compat.1 +25 -0
  162. package/dst/man/ase-util-diagram.1 +76 -0
  163. package/dst/man/ase-util-meta.1 +45 -0
  164. package/dst/man/ase-util-metric.1 +54 -0
  165. package/dst/man/ase-util-mint.1 +59 -0
  166. package/dst/man/ase-util-worktree.1 +72 -0
  167. package/dst/man/ase-util.1 +50 -0
  168. package/dst/man/ase.1 +124 -0
  169. package/package.json +79 -21
  170. package/plugin/.claude-plugin/plugin.json +1 -1
  171. package/plugin/.codex-plugin/plugin.json +1 -1
  172. package/plugin/.github/plugin/plugin.json +1 -1
  173. package/plugin/agents/ase-meta-diagram.md +12 -8
  174. package/plugin/agents/ase-repo-review.md +322 -0
  175. package/plugin/hooks/hooks-codex.json +5 -0
  176. package/plugin/hooks/hooks-copilot.json +5 -0
  177. package/plugin/hooks/hooks.json +8 -0
  178. package/plugin/meta/ase-common-approach.md +117 -0
  179. package/plugin/meta/ase-common-changeset.md +256 -0
  180. package/plugin/meta/ase-common-code.md +10 -173
  181. package/plugin/meta/ase-common-grill.md +317 -0
  182. package/plugin/meta/ase-common-resolve.md +305 -0
  183. package/plugin/meta/ase-common-task.md +83 -4
  184. package/plugin/meta/ase-constitution.md +0 -2
  185. package/plugin/meta/ase-format-specbook.md +208 -25
  186. package/plugin/meta/ase-format-specbook.yaml +242 -851
  187. package/plugin/meta/ase-format-task.md +669 -125
  188. package/plugin/meta/ase-persona.md +3 -4
  189. package/plugin/meta/ase-skill.md +9 -0
  190. package/plugin/mods/ase-mods.ts +409 -0
  191. package/plugin/mods/claude-code.d.ts +13417 -0
  192. package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
  193. package/plugin/skills/ase-arch-analyze/help.md +2 -2
  194. package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
  195. package/plugin/skills/ase-code-analyze/help.md +29 -4
  196. package/plugin/skills/ase-code-edit/SKILL.md +107 -181
  197. package/plugin/skills/ase-code-edit/help.md +118 -31
  198. package/plugin/skills/ase-code-lint/SKILL.md +64 -13
  199. package/plugin/skills/ase-code-lint/help.md +32 -2
  200. package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
  201. package/plugin/skills/ase-docs-proofread/help.md +1 -1
  202. package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
  203. package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
  204. package/plugin/skills/ase-help-intent/data.md +2164 -1087
  205. package/plugin/skills/ase-help-intent/help.md +1 -1
  206. package/plugin/skills/ase-help-skill/catalog.md +12 -9
  207. package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
  208. package/plugin/skills/ase-meta-config/SKILL.md +3 -4
  209. package/plugin/skills/ase-meta-config/help.md +31 -10
  210. package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
  211. package/plugin/skills/ase-meta-mint/help.md +10 -5
  212. package/plugin/skills/ase-meta-why/help.md +1 -1
  213. package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
  214. package/plugin/skills/ase-meta-workflow/help.md +4 -4
  215. package/plugin/skills/ase-meta-workflow/sample.md +3 -3
  216. package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
  217. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
  218. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
  219. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
  220. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
  221. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
  222. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
  223. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
  224. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
  225. package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
  226. package/plugin/skills/ase-repo-merge/help.md +107 -0
  227. package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
  228. package/plugin/skills/ase-repo-resolve/help.md +102 -0
  229. package/plugin/skills/ase-repo-review/SKILL.md +406 -0
  230. package/plugin/skills/ase-repo-review/help.md +181 -0
  231. package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
  232. package/plugin/skills/ase-spec-edit/help.md +107 -26
  233. package/plugin/skills/ase-sync-reconcile/help.md +1 -1
  234. package/plugin/skills/ase-task-assign/SKILL.md +111 -0
  235. package/plugin/skills/ase-task-assign/help.md +119 -0
  236. package/plugin/skills/ase-task-condense/SKILL.md +21 -21
  237. package/plugin/skills/ase-task-condense/help.md +12 -10
  238. package/plugin/skills/ase-task-delete/SKILL.md +1 -1
  239. package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
  240. package/plugin/skills/ase-task-dissect/help.md +10 -9
  241. package/plugin/skills/ase-task-edit/SKILL.md +379 -94
  242. package/plugin/skills/ase-task-edit/help.md +95 -22
  243. package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
  244. package/plugin/skills/ase-task-estimate/help.md +158 -0
  245. package/plugin/skills/ase-task-grill/SKILL.md +246 -138
  246. package/plugin/skills/ase-task-grill/help.md +129 -26
  247. package/plugin/skills/ase-task-id/SKILL.md +8 -1
  248. package/plugin/skills/ase-task-id/help.md +6 -3
  249. package/plugin/skills/ase-task-implement/SKILL.md +535 -145
  250. package/plugin/skills/ase-task-implement/help.md +138 -47
  251. package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
  252. package/plugin/skills/ase-task-integrate/help.md +187 -0
  253. package/plugin/skills/ase-task-list/SKILL.md +55 -23
  254. package/plugin/skills/ase-task-list/help.md +45 -21
  255. package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
  256. package/plugin/skills/ase-task-reboot/help.md +28 -8
  257. package/plugin/skills/ase-task-rename/SKILL.md +10 -2
  258. package/plugin/skills/ase-task-research/SKILL.md +386 -0
  259. package/plugin/skills/ase-task-research/help.md +102 -0
  260. package/plugin/skills/ase-task-status/SKILL.md +137 -0
  261. package/plugin/skills/ase-task-status/help.md +80 -0
  262. package/plugin/skills/ase-task-view/SKILL.md +49 -24
  263. package/plugin/skills/ase-task-view/help.md +19 -9
  264. package/plugin/agents/ase-meta-review.md +0 -187
  265. package/plugin/skills/ase-code-craft/SKILL.md +0 -264
  266. package/plugin/skills/ase-code-craft/help.md +0 -122
  267. package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
  268. package/plugin/skills/ase-code-refactor/help.md +0 -109
  269. package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
  270. package/plugin/skills/ase-code-resolve/help.md +0 -116
  271. package/plugin/skills/ase-meta-review/SKILL.md +0 -210
  272. package/plugin/skills/ase-meta-review/help.md +0 -96
  273. package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
  274. package/plugin/skills/ase-task-preflight/help.md +0 -82
  275. /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
  276. /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
  277. /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
  278. /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
  279. /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
@@ -28,7 +28,6 @@ requested communication style at any time during a session.
28
28
  - You *MUST* output blank lines between paragraphs.
29
29
 
30
30
  - If <ase-persona-style/> is `engineer`, `journalist`, `telegrapher`, or `caveman`:
31
- - You *MUST* use `○` as the bullet point marker symbol in bullet point lists.
32
31
  - You *MUST* *drop* filler words
33
32
  ("just", "really", "basically", "actually", "simply", etc).
34
33
  - You *MUST* *drop* pleasantries
@@ -51,7 +50,7 @@ requested communication style at any time during a session.
51
50
  - You *MUST* *use only* one bullet point per explanation aspect.
52
51
  - You *MUST* structure every bullet point by
53
52
  following exactly the *pyramid* structure and formatting
54
- `○ **<title/>**: <core/> ▶ **<keywords/>**: <details/>`, where:
53
+ `- **<title/>**: <core/> ▶ **<keywords/>**: <details/>`, where:
55
54
  - <title/> is only one or two *words* capturing the aspect
56
55
  - <core/> is a *very terse core* stating the essence of the aspect
57
56
  using one of the following three sentence patterns:
@@ -81,7 +80,7 @@ requested communication style at any time during a session.
81
80
  - If <ase-persona-style/> is `telegrapher`:
82
81
  - You *MUST* structure every bullet point by
83
82
  following exactly the *pyramid* structure and formatting
84
- `○ **<title/>**: <core/>`, where:
83
+ `- **<title/>**: <core/>`, where:
85
84
  - <title/> is only one or two *words* capturing the aspect
86
85
  - <core/> is a *very terse core* stating the essence of the aspect
87
86
  using one of the following three sentence patterns:
@@ -93,7 +92,7 @@ requested communication style at any time during a session.
93
92
 
94
93
  - If <ase-persona-style/> is `caveman`:
95
94
  - You *MUST* structure every bullet point by
96
- following exactly the formatting `○ <core/>`, where:
95
+ following exactly the formatting `- <core/>`, where:
97
96
  <core/> is a *very terse core* stating the essence of the aspect
98
97
  using one of the following four sentence patterns:
99
98
  - `<expression/>!`
@@ -107,6 +107,15 @@ Skill Output
107
107
  Skill Sequential Processing
108
108
  ---------------------------
109
109
 
110
+ - *IMPORTANT*: The task tracking rules below apply *only* if the
111
+ `TaskCreate` and `TaskUpdate` tools are actually available to you.
112
+ If they are *not* available (as with some agent tools or models),
113
+ silently skip *all* task tracking -- the creation of tasks, their
114
+ chaining via `addBlockedBy`, and their status updates -- and do
115
+ *not* search for these tools (e.g. via `ToolSearch`), do *not*
116
+ emulate them, and do *not* mention their absence. The strictly
117
+ sequential processing of every <step/> stays mandatory regardless.
118
+
110
119
  - *IMPORTANT*: For each given <flow/>, you *MUST* use the
111
120
  `TaskCreate` tool to create a corresponding set of processing steps.
112
121
 
@@ -0,0 +1,409 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
+ */
6
+
7
+ /* Claude Code function hooks module ("mod", EARLY ACCESS):
8
+ Grill table, redrawing the grilling tables of ASE with one answer
9
+ alternative per line (wrapped lines indented) and rendering in red
10
+ the answers picked in the prompt via "nX" short responses and the
11
+ question selected in the prompt via the last standalone "n". In the
12
+ latest table, questions and answers are clickable and insert their
13
+ "n" or "nX" into the prompt. */
14
+
15
+ import type { Register, RenderElement } from "claude-code"
16
+
17
+ /* the marker line above a grilling table (single-round and multi-round,
18
+ optionally labeled with the perspective and/or grounding) */
19
+ const MARKER = /⧉ \*{0,2}ASE\*{0,2}: GRILLING(?: ROUND \d+\/\d+)?(?: \([^)]*\))?:/
20
+
21
+ /* a row and the delimiter row of a Markdown table */
22
+ const ROW = /^ *\|.*\| *$/
23
+ const RULE = /^ *\|(?: *:?-+:? *\|)+ *$/
24
+
25
+ /* the separator between two answers, the letter of
26
+ an answer, and the number of a question */
27
+ const ANSWER = /,\s+(?=\*{0,2}[A-Z]\*{0,2}\s+▶)/
28
+ const LETTER = /^\*{0,2}([A-Z])\*{0,2}\s+▶/
29
+ const NUMBER = /^\*{0,2}(\d+)\*{0,2}\s+▶/
30
+
31
+ /* a short response "nX" inside the prompt */
32
+ const PICK = /(?<![^\s,])(\d+)([a-zA-Z])(?![^\s,])/g
33
+
34
+ /* a standalone question number "n" inside the prompt */
35
+ const SELECT = /(?<![^\s,])(\d+)(?![^\s,])/g
36
+
37
+ /* the indent of the wrapped lines of an answer, the colors of a picked answer
38
+ and of a code span, and the size limit of a single "Markdown" element */
39
+ const INDENT = 4
40
+ const PICKED = "red"
41
+ const CODE = "blue"
42
+ const LIMIT = 10000
43
+
44
+ /* a single styled character of a table cell */
45
+ type Char = { ch: string, bold: boolean, color: string | undefined }
46
+
47
+ /* a grilling table with the Markdown text before and after it */
48
+ type Table = {
49
+ before: string,
50
+ after: string,
51
+ header: string[],
52
+ rows: { number: number, question: string, answers: { letter: string, text: string }[] }[]
53
+ }
54
+
55
+ /* split a Markdown table row into its cells */
56
+ const cells = (line: string): string[] => {
57
+ const result: string[] = []
58
+ const body = line.trim().replace(/^\|/, "").replace(/\|$/, "")
59
+ let cell = ""
60
+ let code = false
61
+ for (let i = 0; i < body.length; i++) {
62
+ const ch = body[i]!
63
+ if (ch === "`")
64
+ code = !code
65
+ if (!code && ch === "\\" && body[i + 1] === "|") {
66
+ cell += "|"
67
+ i++
68
+ }
69
+ else if (!code && ch === "|") {
70
+ result.push(cell.trim())
71
+ cell = ""
72
+ }
73
+ else
74
+ cell += ch
75
+ }
76
+ result.push(cell.trim())
77
+ return result
78
+ }
79
+
80
+ /* parse the grilling table out of the text of a reply */
81
+ const parse = (text: string): Table | undefined => {
82
+ const lines = text.split("\n")
83
+ const m = lines.findIndex((line) => MARKER.test(line))
84
+ const h = lines.findIndex((line, i) => m !== -1 && i > m && ROW.test(line))
85
+ if (h === -1 || !RULE.test(lines[h + 1] ?? ""))
86
+ return undefined
87
+ const header = cells(lines[h]!).map((cell) => cell.replace(/\*/g, ""))
88
+ if (header.length !== 2 || header[0] !== "QUESTION" || !/^ANSWERS?$/.test(header[1]!))
89
+ return undefined
90
+ let end = h + 2
91
+ while (end < lines.length && ROW.test(lines[end]!))
92
+ end++
93
+ const rows = lines.slice(h + 2, end).map((line) => cells(line))
94
+ if (rows.some((row) => row.length !== 2))
95
+ return undefined
96
+ return {
97
+ before: lines.slice(0, h).join("\n").trim(),
98
+ after: lines.slice(end).join("\n").replace(/^\n+/, "").trimEnd(),
99
+ header,
100
+ rows: rows.map((row, i) => ({
101
+ number: Number(NUMBER.exec(row[0]!)?.[1] ?? i + 1),
102
+ question: row[0]!,
103
+ answers: row[1]!.split(ANSWER).map((answer) =>
104
+ ({ letter: LETTER.exec(answer)?.[1] ?? "", text: answer }))
105
+ }))
106
+ }
107
+ }
108
+
109
+ /* determine the picked answer letter of each question out of a prompt (last pick wins) */
110
+ const pick = (text: string): Map<number, string> =>
111
+ new Map([ ...text.matchAll(PICK) ].map((m) => [ Number(m[1]!), m[2]!.toUpperCase() ]))
112
+
113
+ /* determine the selected question number out of a prompt (last standalone number wins) */
114
+ const select = (text: string): number | undefined => {
115
+ const m = [ ...text.matchAll(SELECT) ].at(-1)
116
+ return m !== undefined ? Number(m[1]!) : undefined
117
+ }
118
+
119
+ /* the standalone tokens of question "n" inside the prompt: its bare
120
+ number "n" (letter "") or its short responses "nX" (letter "[a-zA-Z]") */
121
+ const token = (number: number, letter: string): RegExp =>
122
+ new RegExp(`(?<![^\\s,])${number}${letter}(?![^\\s,])`, "g")
123
+
124
+ /* insert the number "n" of a clicked question or the short response "nX" of a
125
+ clicked answer into a prompt. Answer: an existing "nY" is replaced in place
126
+ (and a bare "n" removed), else a bare "n" is replaced in place, else "nX" is
127
+ appended. Question: an existing bare "n" is kept, else the bare number of
128
+ another question (there is at most one selected question) is replaced in
129
+ place, else "n" is appended. */
130
+ const insert = (text: string, number: number, letter?: string): string => {
131
+ const bare = token(number, "")
132
+ const picks = token(number, "[a-zA-Z]")
133
+ if (letter !== undefined && text.search(picks) !== -1)
134
+ return text.replace(picks, `${number}${letter}`)
135
+ .replace(new RegExp(`\\s*${bare.source}`, "g"), "").trimStart()
136
+ if (text.search(bare) !== -1)
137
+ return letter !== undefined ? text.replace(bare, `${number}${letter}`) : text
138
+ if (letter === undefined && text.search(SELECT) !== -1)
139
+ return text.replace(SELECT, String(number))
140
+ const head = text.trimEnd()
141
+ return `${head}${head !== "" ? " " : ""}${number}${letter ?? ""}`
142
+ }
143
+
144
+ /* convert inline Markdown (bold and code spans) into styled characters */
145
+ const style = (text: string, color: string | undefined): Char[] => {
146
+ const chars: Char[] = []
147
+ let bold = false
148
+ let code = false
149
+ for (let i = 0; i < text.length; i++) {
150
+ if (!code && text.startsWith("**", i)) {
151
+ bold = !bold
152
+ i++
153
+ }
154
+ else if (text[i] === "`")
155
+ code = !code
156
+ else {
157
+ const ch = String.fromCodePoint(text.codePointAt(i)!)
158
+ chars.push({ ch, bold, color: color ?? (code ? CODE : undefined) })
159
+ i += ch.length - 1
160
+ }
161
+ }
162
+ return chars
163
+ }
164
+
165
+ /* the width of styled characters in terminal cells */
166
+ const width = (chars: Char[]): number => {
167
+ let n = 0
168
+ for (const { ch } of chars) {
169
+ const c = ch.codePointAt(0)!
170
+ if (c === 0x200B || c === 0x2060 || (c >= 0x0300 && c <= 0x036F) || (c >= 0xFE00 && c <= 0xFE0F))
171
+ continue
172
+ const wide =
173
+ (c >= 0x1100 && c <= 0x115F) || (c >= 0x2E80 && c <= 0xA4CF) ||
174
+ (c >= 0xAC00 && c <= 0xD7A3) || (c >= 0xF900 && c <= 0xFAFF) ||
175
+ (c >= 0xFF00 && c <= 0xFF60) || (c >= 0x1F300 && c <= 0x1FAFF)
176
+ n += wide ? 2 : 1
177
+ }
178
+ return n
179
+ }
180
+
181
+ /* hard-wrap styled characters at word boundaries to the given columns,
182
+ indenting the first line by "lead" and all further lines by "indent"
183
+ (and chopping overlong words) */
184
+ const wrap = (chars: Char[], columns: number, indent: number, lead = 0): Char[][] => {
185
+ const space: Char = { ch: " ", bold: false, color: undefined }
186
+ const words: Char[][] = [ [] ]
187
+ for (const char of chars) {
188
+ if (char.ch === " ")
189
+ words.push([])
190
+ else
191
+ words.at(-1)!.push(char)
192
+ }
193
+ const lines: Char[][] = []
194
+ let line: Char[] = Array.from({ length: lead }, () => space)
195
+ let start = lead
196
+ let size = lead
197
+ const flush = () => {
198
+ lines.push(line)
199
+ line = Array.from({ length: indent }, () => space)
200
+ start = indent
201
+ size = indent
202
+ }
203
+ for (let word of words.filter((word) => word.length > 0)) {
204
+ while (word.length > 0) {
205
+ const w = width(word)
206
+ if (size > start && size + 1 + w <= columns) {
207
+ line.push(space, ...word)
208
+ size += 1 + w
209
+ word = []
210
+ }
211
+ else if (size > start)
212
+ flush()
213
+ else {
214
+ let n = 1
215
+ while (n < word.length && width(word.slice(0, n + 1)) <= columns - start)
216
+ n++
217
+ line.push(...word.slice(0, n))
218
+ size += width(word.slice(0, n))
219
+ word = word.slice(n)
220
+ if (word.length > 0)
221
+ flush()
222
+ }
223
+ }
224
+ }
225
+ lines.push(line)
226
+ return lines
227
+ }
228
+
229
+ /* function hook registration entry point */
230
+ export const register: Register = (on, options) => {
231
+ /* allow the rendering to be disabled without disabling the plugin */
232
+ if (options.enabled === false)
233
+ return
234
+
235
+ /* the last known width of the terminal, the already seen tables, the
236
+ latest (still unanswered) table, the picks and the selected question of the
237
+ still unsubmitted prompt, and the frozen picks of the already answered tables */
238
+ let columns = 80
239
+ const seen = new Set<string>()
240
+ let latest: string | undefined
241
+ let picks = new Map<number, string>()
242
+ let selected: number | undefined
243
+ const frozen = new Map<string, Map<number, string>>()
244
+
245
+ /* track the picks inside the still unsubmitted prompt of the user */
246
+ on("prompt.edit", async ($, e, next) => {
247
+ const box = await next(e)
248
+ const now = pick(box.text)
249
+ const sel = select(box.text)
250
+ if (JSON.stringify([ ...now ]) !== JSON.stringify([ ...picks ]) || sel !== selected) {
251
+ picks = now
252
+ selected = sel
253
+ $.ui.invalidate("ui.render")
254
+ }
255
+ return box
256
+ })
257
+
258
+ /* freeze the picks in the latest table once the prompt is submitted */
259
+ on("prompt.submit", ($, e, next) => {
260
+ if (latest !== undefined)
261
+ frozen.set(latest, pick(e.text))
262
+ latest = undefined
263
+ picks = new Map()
264
+ selected = undefined
265
+ $.ui.invalidate("ui.render")
266
+ return next(e)
267
+ })
268
+
269
+ /* redraw the replies which contain a grilling table (terminal only) */
270
+ on("ui.render", { component: "AssistantMessage", surface: "terminal" }, async ($, e, next) => {
271
+ /* act on grilling tables only, which fit into the terminal
272
+ (gutter, grid lines, cell paddings, and a spare column subtracted) */
273
+ columns = e.viewport?.columns ?? columns
274
+ const table = parse(e.props.text)
275
+ const avail = columns - 2 - 7 - 1
276
+ if (table === undefined || avail < 40 || table.before.length > LIMIT || table.after.length > LIMIT)
277
+ return next(e)
278
+
279
+ /* let the chain beneath run, although its drawing is replaced by us
280
+ (but this at least silences a warning in the debug output of Claude Code) */
281
+ await next(e)
282
+
283
+ /* determine the picks: frozen ones or, for the latest table, the live ones */
284
+ if (!seen.has(e.requestId)) {
285
+ seen.add(e.requestId)
286
+ latest = e.requestId
287
+ }
288
+ const chosen = frozen.get(e.requestId) ?? (e.requestId === latest ? picks : undefined)
289
+ const marked = e.requestId === latest ? selected : undefined
290
+
291
+ /* style the cells and determine the column widths, right-aligning the
292
+ question numbers to the widest one (so the wrapped lines of all
293
+ questions share the same indent) */
294
+ const head = table.header.map((cell) => style(`**${cell}**`, undefined))
295
+ const digits = Math.max(...table.rows.map((row) => String(row.number).length))
296
+ const rows = table.rows.map((row) => {
297
+ const numbered = NUMBER.test(row.question)
298
+ return {
299
+ lead: numbered ? digits - String(row.number).length : 0,
300
+ indent: numbered ? digits + 3 : 0,
301
+ question: style(row.question, marked === row.number ? PICKED : undefined),
302
+ answers: row.answers.map((answer) =>
303
+ style(answer.text, chosen?.get(row.number) === answer.letter ? PICKED : undefined))
304
+ }
305
+ })
306
+ const w1nat = Math.max(width(head[0]!), ...rows.map((row) => row.lead + width(row.question)))
307
+ const w2nat = Math.max(width(head[1]!), ...rows.flatMap((row) => row.answers.map((answer) => width(answer))))
308
+ let w1 = Math.min(w1nat, Math.floor(avail * 0.4))
309
+ const w2 = Math.min(w2nat, avail - w1)
310
+ w1 = Math.min(w1nat, avail - w2)
311
+
312
+ /* resolve the UI elements */
313
+ const { Box, Text, Button, Markdown } = $.ui.resolve(e)
314
+
315
+ /* split a line of a cell into runs of equally styled characters */
316
+ const split = (chars: Char[]): { text: string, bold: boolean, color: string | undefined }[] => {
317
+ const runs: { text: string, bold: boolean, color: string | undefined }[] = []
318
+ for (let i = 0, j = 0; i < chars.length; i = j) {
319
+ const { bold, color } = chars[i]!
320
+ while (j < chars.length && chars[j]!.bold === bold && chars[j]!.color === color)
321
+ j++
322
+ runs.push({ text: chars.slice(i, j).map((char) => char.ch).join(""), bold, color })
323
+ }
324
+ return runs
325
+ }
326
+
327
+ /* draw a line of a cell: its styled runs, padded to the column width */
328
+ const paint = (chars: Char[], columns: number): (string | RenderElement)[] => [
329
+ ...split(chars).map(({ text, bold, color }) =>
330
+ bold || color !== undefined ? Text({ bold, color, children: text }) : text),
331
+ " ".repeat(Math.max(0, columns - width(chars)))
332
+ ]
333
+
334
+ /* insert the number of a clicked question or the short response of a clicked
335
+ answer into the prompt and refresh the picks and the selection from it */
336
+ const press = (number: number, letter?: string) => () => {
337
+ void $.prompt.read()
338
+ .then(({ text }) => $.prompt.fill({ text: insert(text, number, letter) }))
339
+ .then((box) => {
340
+ picks = pick(box.text)
341
+ selected = select(box.text)
342
+ $.ui.invalidate("ui.render")
343
+ })
344
+ }
345
+
346
+ /* draw a line of a cell: in the latest table as the styled text inside a
347
+ hover-scoping box, which while any line of its group (all lines of the
348
+ same question or answer) is hovered reveals a row of clickable plain
349
+ buttons (one per styled run, inversed in the default colors and carrying
350
+ the run's bold style, but not its color, as its hover style, because the
351
+ engine leaves the space cells uncolored) on top of it; otherwise as
352
+ styled text only */
353
+ const draw = (chars: Char[], columns: number, group: string, line: number,
354
+ onPress: () => void): RenderElement[] => {
355
+ const w = width(chars)
356
+ if (e.requestId !== latest || w === 0)
357
+ return [ Text({ children: paint(chars, columns) }) ]
358
+ const buttons = split(chars).map(({ text, bold }, r) =>
359
+ Button({ key: `${group}/${line}/${r}`, label: text, plain: true, onPress,
360
+ hover: { scope: group, inverse: true, bold } }))
361
+ return [
362
+ Box({ key: `${group}/${line}`, hover: { scope: group }, children: [
363
+ Text({ children: paint(chars, w) }),
364
+ Box({ position: "absolute", top: 0, left: 0, display: "none",
365
+ hover: { scope: group, display: "flex" }, flexDirection: "row", children: buttons })
366
+ ] }),
367
+ Text({ children: " ".repeat(Math.max(0, columns - w)) })
368
+ ]
369
+ }
370
+
371
+ /* draw the table as a grid: a rule between all rows and one answer per line */
372
+ const rule = (l: string, m: string, r: string) =>
373
+ Text({ wrap: "truncate-end", children: l + "─".repeat(w1 + 2) + m + "─".repeat(w2 + 2) + r })
374
+ const grid: RenderElement[] = [ rule("┌", "┬", "┐") ]
375
+ grid.push(Text({ wrap: "truncate-end", children:
376
+ [ "│ ", ...paint(head[0]!, w1), " │ ", ...paint(head[1]!, w2), " │" ] }))
377
+ grid.push(rule("├", "┼", "┤"))
378
+ rows.forEach((row, k) => {
379
+ const number = table.rows[k]!.number
380
+ const q = wrap(row.question, w1, row.indent, row.lead).map((line, i) =>
381
+ draw(line, w1, `q${number}`, i, press(number)))
382
+ const a = row.answers.flatMap((answer, j) => {
383
+ const letter = table.rows[k]!.answers[j]!.letter
384
+ return wrap(answer, w2, INDENT).map((line, i) =>
385
+ draw(line, w2, `a${number}${letter}`, i, press(number, letter)))
386
+ })
387
+ for (let i = 0; i < Math.max(q.length, a.length); i++)
388
+ grid.push(Box({ flexDirection: "row", children: [
389
+ Text({ children: "│ " }),
390
+ ...(q[i] ?? [ Text({ children: " ".repeat(w1) }) ]),
391
+ Text({ children: " │ " }),
392
+ ...(a[i] ?? [ Text({ children: " ".repeat(w2) }) ]),
393
+ Text({ children: " │" })
394
+ ] }))
395
+ grid.push(k < rows.length - 1 ? rule("├", "┼", "┤") : rule("└", "┴", "┘"))
396
+ })
397
+
398
+ /* mimic the reply row of the engine: blank row above and bullet gutter */
399
+ return Box({ flexDirection: "row", marginTop: 1, width: "100%", children: [
400
+ Box({ flexShrink: 0, minWidth: 2, children: Text({ children: e.props.isFirstOfReply ? "⏺" : " " }) }),
401
+ Box({ flexDirection: "column", flexGrow: 1, flexShrink: 1, children: [
402
+ ...(table.before !== "" ? [ Markdown({ text: table.before }) ] : []),
403
+ Box({ flexDirection: "column", marginTop: table.before !== "" ? 1 : 0, children: grid }),
404
+ ...(table.after !== "" ? [ Box({ marginTop: 1, children: Markdown({ text: table.after }) }) ] : [])
405
+ ] })
406
+ ] })
407
+ })
408
+ }
409
+