@rse/ase 2.0.3 → 2.0.5

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 (198) hide show
  1. package/dst/ase-hook.js +13 -4
  2. package/dst/ase-lib-version.js +14 -5
  3. package/dst/ase-man-catalog.js +229 -0
  4. package/dst/ase-man-render.js +278 -0
  5. package/dst/ase-man-tui.js +636 -0
  6. package/dst/ase-man.js +33 -27
  7. package/dst/ase-setup.js +2 -2
  8. package/dst/ase-spec.js +103 -20
  9. package/dst/ase-task-board-filter.js +5 -4
  10. package/dst/ase-task-board-web-client.js +26 -26
  11. package/dst/man/ase-agent-adhoc.md +1165 -0
  12. package/dst/man/ase-agent-condenser.md +383 -0
  13. package/dst/man/ase-agent-editor.md +441 -0
  14. package/dst/man/ase-agent-estimator.md +477 -0
  15. package/dst/man/ase-agent-fleet.md +358 -0
  16. package/dst/man/ase-agent-implementer.md +521 -0
  17. package/dst/man/ase-agent-integrator.md +449 -0
  18. package/dst/man/ase-agent-qualifier.md +426 -0
  19. package/dst/man/ase-agent-rebooter.md +384 -0
  20. package/dst/man/ase-agent-relay.md +262 -0
  21. package/dst/man/ase-agent-researcher.md +478 -0
  22. package/dst/man/ase-agent.md +628 -0
  23. package/dst/man/ase-artifact-list.md +57 -0
  24. package/dst/man/ase-artifact-name.md +56 -0
  25. package/dst/man/ase-artifact.md +92 -0
  26. package/dst/man/ase-config-delete.md +74 -0
  27. package/dst/man/ase-config-edit.md +83 -0
  28. package/dst/man/ase-config-get.md +70 -0
  29. package/dst/man/ase-config-init.md +110 -0
  30. package/dst/man/ase-config-list.md +68 -0
  31. package/dst/man/ase-config-set.md +92 -0
  32. package/dst/man/ase-config.md +426 -0
  33. package/dst/man/ase-hook-permission-request.md +61 -0
  34. package/dst/man/ase-hook-pre-tool-use.md +96 -0
  35. package/dst/man/ase-hook-session-end.md +71 -0
  36. package/dst/man/ase-hook-session-persona.md +65 -0
  37. package/dst/man/ase-hook-session-start.md +184 -0
  38. package/dst/man/ase-hook-stop.md +52 -0
  39. package/dst/man/ase-hook-user-prompt-submit.md +54 -0
  40. package/dst/man/ase-hook.md +93 -0
  41. package/dst/man/ase-man.md +195 -0
  42. package/dst/man/ase-mcp.md +102 -0
  43. package/dst/man/ase-service-log.md +59 -0
  44. package/dst/man/ase-service-send.md +60 -0
  45. package/dst/man/ase-service-start.md +93 -0
  46. package/dst/man/ase-service-status.md +58 -0
  47. package/dst/man/ase-service-stop.md +52 -0
  48. package/dst/man/ase-service.md +124 -0
  49. package/dst/man/ase-setup-disable.md +67 -0
  50. package/dst/man/ase-setup-enable.md +66 -0
  51. package/dst/man/ase-setup-install.md +116 -0
  52. package/dst/man/ase-setup-mcp.md +213 -0
  53. package/dst/man/ase-setup-status.md +101 -0
  54. package/dst/man/ase-setup-statusline.md +162 -0
  55. package/dst/man/ase-setup-telemetry.md +166 -0
  56. package/dst/man/ase-setup-uninstall.md +85 -0
  57. package/dst/man/ase-setup-update.md +117 -0
  58. package/dst/man/ase-setup.md +156 -0
  59. package/dst/man/ase-spec-export.md +99 -0
  60. package/dst/man/ase-spec-init.md +57 -0
  61. package/dst/man/ase-spec-lint.md +74 -0
  62. package/dst/man/ase-spec-preview.md +57 -0
  63. package/dst/man/ase-spec.md +85 -0
  64. package/dst/man/ase-statusline.md +330 -0
  65. package/dst/man/ase-task-assign.md +138 -0
  66. package/dst/man/ase-task-board.md +651 -0
  67. package/dst/man/ase-task-delete.md +46 -0
  68. package/dst/man/ase-task-edit.md +73 -0
  69. package/dst/man/ase-task-idscheme.md +92 -0
  70. package/dst/man/ase-task-lifecycle.md +92 -0
  71. package/dst/man/ase-task-list.md +86 -0
  72. package/dst/man/ase-task-load.md +48 -0
  73. package/dst/man/ase-task-newid.md +100 -0
  74. package/dst/man/ase-task-purge.md +49 -0
  75. package/dst/man/ase-task-rename.md +69 -0
  76. package/dst/man/ase-task-request.md +108 -0
  77. package/dst/man/ase-task-save.md +78 -0
  78. package/dst/man/ase-task-status.md +79 -0
  79. package/dst/man/ase-task-store.md +377 -0
  80. package/dst/man/ase-task-view.md +50 -0
  81. package/dst/man/ase-task.md +335 -0
  82. package/dst/man/ase-util-compat.md +38 -0
  83. package/dst/man/ase-util-diagram.md +140 -0
  84. package/dst/man/ase-util-meta.md +68 -0
  85. package/dst/man/ase-util-metric.md +74 -0
  86. package/dst/man/ase-util-mint.md +85 -0
  87. package/dst/man/ase-util-worktree.md +109 -0
  88. package/dst/man/ase-util.md +81 -0
  89. package/dst/man/ase.md +228 -0
  90. package/package.json +16 -21
  91. package/plugin/.claude-plugin/plugin.json +1 -1
  92. package/plugin/.codex-plugin/plugin.json +1 -1
  93. package/plugin/.github/plugin/plugin.json +1 -1
  94. package/plugin/agents/ase-code-lint.md +3 -3
  95. package/plugin/agents/ase-docs-proofread.md +3 -3
  96. package/plugin/agents/ase-docs-refine.md +3 -3
  97. package/plugin/agents/ase-spec-lint.md +338 -0
  98. package/plugin/mods/ase-mods-common.ts +444 -0
  99. package/plugin/mods/ase-mods-grill.ts +312 -0
  100. package/plugin/mods/ase-mods-help-browse.ts +405 -0
  101. package/plugin/mods/ase-mods-help-compose.ts +561 -0
  102. package/plugin/mods/ase-mods-help-find.ts +426 -0
  103. package/plugin/mods/ase-mods-help-hint.ts +134 -0
  104. package/plugin/mods/ase-mods-help-show.ts +142 -0
  105. package/plugin/mods/ase-mods.ts +214 -381
  106. package/plugin/mods/claude-code.d.ts +26 -3
  107. package/plugin/skills/ase-code-lint/SKILL.md +15 -15
  108. package/plugin/skills/ase-code-lint/help.md +21 -8
  109. package/plugin/skills/ase-help-intent/SKILL.md +2 -1
  110. package/plugin/skills/ase-help-intent/data.md +214 -9
  111. package/plugin/skills/ase-help-skill/catalog.md +1 -0
  112. package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
  113. package/plugin/skills/ase-repo-commit/SKILL.md +1 -0
  114. package/plugin/skills/ase-repo-merge/SKILL.md +1 -0
  115. package/plugin/skills/ase-spec-edit/SKILL.md +3 -3
  116. package/plugin/skills/ase-spec-edit/help.md +1 -1
  117. package/plugin/skills/ase-spec-lint/SKILL.md +695 -0
  118. package/plugin/skills/ase-spec-lint/help.md +184 -0
  119. package/plugin/skills/ase-task-implement/SKILL.md +24 -6
  120. package/plugin/skills/ase-task-implement/help.md +11 -1
  121. package/dst/man/ase-agent-adhoc.1 +0 -515
  122. package/dst/man/ase-agent-condenser.1 +0 -193
  123. package/dst/man/ase-agent-editor.1 +0 -212
  124. package/dst/man/ase-agent-estimator.1 +0 -214
  125. package/dst/man/ase-agent-fleet.1 +0 -185
  126. package/dst/man/ase-agent-implementer.1 +0 -233
  127. package/dst/man/ase-agent-integrator.1 +0 -213
  128. package/dst/man/ase-agent-qualifier.1 +0 -214
  129. package/dst/man/ase-agent-rebooter.1 +0 -193
  130. package/dst/man/ase-agent-relay.1 +0 -128
  131. package/dst/man/ase-agent-researcher.1 +0 -217
  132. package/dst/man/ase-agent.1 +0 -264
  133. package/dst/man/ase-artifact-list.1 +0 -38
  134. package/dst/man/ase-artifact-name.1 +0 -39
  135. package/dst/man/ase-artifact.1 +0 -56
  136. package/dst/man/ase-config-delete.1 +0 -45
  137. package/dst/man/ase-config-edit.1 +0 -48
  138. package/dst/man/ase-config-get.1 +0 -46
  139. package/dst/man/ase-config-init.1 +0 -60
  140. package/dst/man/ase-config-list.1 +0 -42
  141. package/dst/man/ase-config-set.1 +0 -52
  142. package/dst/man/ase-config.1 +0 -231
  143. package/dst/man/ase-hook-permission-request.1 +0 -36
  144. package/dst/man/ase-hook-pre-tool-use.1 +0 -59
  145. package/dst/man/ase-hook-session-end.1 +0 -47
  146. package/dst/man/ase-hook-session-persona.1 +0 -37
  147. package/dst/man/ase-hook-session-start.1 +0 -81
  148. package/dst/man/ase-hook-stop.1 +0 -34
  149. package/dst/man/ase-hook-user-prompt-submit.1 +0 -34
  150. package/dst/man/ase-hook.1 +0 -51
  151. package/dst/man/ase-man.1 +0 -67
  152. package/dst/man/ase-mcp.1 +0 -61
  153. package/dst/man/ase-service-log.1 +0 -43
  154. package/dst/man/ase-service-send.1 +0 -47
  155. package/dst/man/ase-service-start.1 +0 -54
  156. package/dst/man/ase-service-status.1 +0 -46
  157. package/dst/man/ase-service-stop.1 +0 -37
  158. package/dst/man/ase-service.1 +0 -85
  159. package/dst/man/ase-setup-disable.1 +0 -44
  160. package/dst/man/ase-setup-enable.1 +0 -45
  161. package/dst/man/ase-setup-install.1 +0 -73
  162. package/dst/man/ase-setup-mcp.1 +0 -124
  163. package/dst/man/ase-setup-status.1 +0 -60
  164. package/dst/man/ase-setup-statusline.1 +0 -92
  165. package/dst/man/ase-setup-telemetry.1 +0 -92
  166. package/dst/man/ase-setup-uninstall.1 +0 -59
  167. package/dst/man/ase-setup-update.1 +0 -64
  168. package/dst/man/ase-setup.1 +0 -93
  169. package/dst/man/ase-spec-export.1 +0 -62
  170. package/dst/man/ase-spec-lint.1 +0 -40
  171. package/dst/man/ase-spec-preview.1 +0 -40
  172. package/dst/man/ase-spec.1 +0 -46
  173. package/dst/man/ase-statusline.1 +0 -189
  174. package/dst/man/ase-task-assign.1 +0 -71
  175. package/dst/man/ase-task-board.1 +0 -384
  176. package/dst/man/ase-task-delete.1 +0 -35
  177. package/dst/man/ase-task-edit.1 +0 -46
  178. package/dst/man/ase-task-idscheme.1 +0 -65
  179. package/dst/man/ase-task-lifecycle.1 +0 -57
  180. package/dst/man/ase-task-list.1 +0 -47
  181. package/dst/man/ase-task-load.1 +0 -36
  182. package/dst/man/ase-task-newid.1 +0 -61
  183. package/dst/man/ase-task-purge.1 +0 -37
  184. package/dst/man/ase-task-rename.1 +0 -44
  185. package/dst/man/ase-task-request.1 +0 -61
  186. package/dst/man/ase-task-save.1 +0 -48
  187. package/dst/man/ase-task-status.1 +0 -55
  188. package/dst/man/ase-task-store.1 +0 -194
  189. package/dst/man/ase-task-view.1 +0 -40
  190. package/dst/man/ase-task.1 +0 -160
  191. package/dst/man/ase-util-compat.1 +0 -25
  192. package/dst/man/ase-util-diagram.1 +0 -76
  193. package/dst/man/ase-util-meta.1 +0 -45
  194. package/dst/man/ase-util-metric.1 +0 -54
  195. package/dst/man/ase-util-mint.1 +0 -59
  196. package/dst/man/ase-util-worktree.1 +0 -72
  197. package/dst/man/ase-util.1 +0 -50
  198. package/dst/man/ase.1 +0 -124
@@ -0,0 +1,312 @@
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 { Elements, Frozen, MatchedEvent, RenderElement } from "claude-code"
16
+ import { type Engine, type Char, PICKED, style, width, wrap, split, paint } from "./ase-mods-common.ts"
17
+
18
+ /* the marker line above a grilling table (single-round and multi-round,
19
+ optionally labeled with the perspective and/or grounding) */
20
+ const MARKER = /⧉ \*{0,2}ASE\*{0,2}: GRILLING(?: ROUND \d+\/\d+)?(?: \([^)]*\))?:/
21
+
22
+ /* a row and the delimiter row of a Markdown table */
23
+ const ROW = /^ *\|.*\| *$/
24
+ const RULE = /^ *\|(?: *:?-+:? *\|)+ *$/
25
+
26
+ /* the separator between two answers, the letter of
27
+ an answer, and the number of a question */
28
+ const ANSWER = /,\s+(?=\*{0,2}[A-Z]\*{0,2}\s+▶)/
29
+ const LETTER = /^\*{0,2}([A-Z])\*{0,2}\s+▶/
30
+ const NUMBER = /^\*{0,2}(\d+)\*{0,2}\s+▶/
31
+
32
+ /* a short response "nX" inside the prompt */
33
+ const PICK = /(?<![^\s,])(\d+)([a-zA-Z])(?![^\s,])/g
34
+
35
+ /* a standalone question number "n" inside the prompt */
36
+ const SELECT = /(?<![^\s,])(\d+)(?![^\s,])/g
37
+
38
+ /* the indent of the wrapped lines of an answer
39
+ and the size limit of a single "Markdown" element */
40
+ const INDENT = 4
41
+ const LIMIT = 10000
42
+
43
+ /* a grilling table with the Markdown text before and after it */
44
+ type Table = {
45
+ before: string,
46
+ after: string,
47
+ header: string[],
48
+ rows: { number: number, question: string, answers: { letter: string, text: string }[] }[]
49
+ }
50
+
51
+ /* split a Markdown table row into its cells */
52
+ const cells = (line: string): string[] => {
53
+ const result: string[] = []
54
+ const body = line.trim().replace(/^\|/, "").replace(/\|$/, "")
55
+ let cell = ""
56
+ let code = false
57
+ for (let i = 0; i < body.length; i++) {
58
+ const ch = body[i]!
59
+ if (ch === "`")
60
+ code = !code
61
+ if (!code && ch === "\\" && body[i + 1] === "|") {
62
+ cell += "|"
63
+ i++
64
+ }
65
+ else if (!code && ch === "|") {
66
+ result.push(cell.trim())
67
+ cell = ""
68
+ }
69
+ else
70
+ cell += ch
71
+ }
72
+ result.push(cell.trim())
73
+ return result
74
+ }
75
+
76
+ /* parse the grilling table out of the text of a reply */
77
+ const parse = (text: string): Table | undefined => {
78
+ const lines = text.split("\n")
79
+ const m = lines.findIndex((line) => MARKER.test(line))
80
+ const h = lines.findIndex((line, i) => m !== -1 && i > m && ROW.test(line))
81
+ if (h === -1 || !RULE.test(lines[h + 1] ?? ""))
82
+ return undefined
83
+ const header = cells(lines[h]!).map((cell) => cell.replace(/\*/g, ""))
84
+ if (header.length !== 2 || header[0] !== "QUESTION" || !/^ANSWERS?$/.test(header[1]!))
85
+ return undefined
86
+ let end = h + 2
87
+ while (end < lines.length && ROW.test(lines[end]!))
88
+ end++
89
+ const rows = lines.slice(h + 2, end).map((line) => cells(line))
90
+ if (rows.length === 0 || rows.some((row) => row.length !== 2))
91
+ return undefined
92
+ return {
93
+ before: lines.slice(0, h).join("\n").trim(),
94
+ after: lines.slice(end).join("\n").replace(/^\n+/, "").trimEnd(),
95
+ header,
96
+ rows: rows.map((row, i) => ({
97
+ number: Number(NUMBER.exec(row[0]!)?.[1] ?? i + 1),
98
+ question: row[0]!,
99
+ answers: row[1]!.split(ANSWER).map((answer) =>
100
+ ({ letter: LETTER.exec(answer)?.[1] ?? "", text: answer }))
101
+ }))
102
+ }
103
+ }
104
+
105
+ /* determine the picked answer letter of each question out of a prompt (last pick wins) */
106
+ const pick = (text: string): Map<number, string> =>
107
+ new Map([ ...text.matchAll(PICK) ].map((m) => [ Number(m[1]!), m[2]!.toUpperCase() ]))
108
+
109
+ /* determine the selected question number out of a prompt (last standalone number wins) */
110
+ const select = (text: string): number | undefined => {
111
+ const m = [ ...text.matchAll(SELECT) ].at(-1)
112
+ return m !== undefined ? Number(m[1]!) : undefined
113
+ }
114
+
115
+ /* the standalone tokens of question "n" inside the prompt: its bare
116
+ number "n" (letter "") or its short responses "nX" (letter "[a-zA-Z]") */
117
+ const token = (number: number, letter: string): RegExp =>
118
+ new RegExp(`(?<![^\\s,])${number}${letter}(?![^\\s,])`, "g")
119
+
120
+ /* insert the number "n" of a clicked question or the short response "nX" of a
121
+ clicked answer into a prompt. Answer: an existing "nY" is replaced in place
122
+ (and a bare "n" removed), else a bare "n" is replaced in place, else "nX" is
123
+ appended. Question: an existing bare "n" is kept, else the last bare number
124
+ of another question (the selected one) is replaced in place, else "n" is
125
+ appended. */
126
+ const insert = (text: string, number: number, letter?: string): string => {
127
+ const bare = token(number, "")
128
+ const picks = token(number, "[a-zA-Z]")
129
+ if (letter !== undefined && text.search(picks) !== -1)
130
+ return text.replace(picks, `${number}${letter}`)
131
+ .replace(new RegExp(`\\s*${bare.source}`, "g"), "").trimStart()
132
+ if (text.search(bare) !== -1)
133
+ return letter !== undefined ? text.replace(bare, `${number}${letter}`) : text
134
+ const last = letter === undefined ? [ ...text.matchAll(SELECT) ].at(-1) : undefined
135
+ if (last !== undefined)
136
+ return text.slice(0, last.index) + String(number) + text.slice(last.index + last[0].length)
137
+ const head = text.trimEnd()
138
+ return `${head}${head !== "" ? " " : ""}${number}${letter ?? ""}`
139
+ }
140
+
141
+ /* style the cells of a table and determine its column widths within the available
142
+ columns, right-aligning the question numbers to the widest one (so the wrapped
143
+ lines of all questions share the same indent) */
144
+ const layout = (table: Table, avail: number,
145
+ chosen: Map<number, string> | undefined, marked: number | undefined) => {
146
+ const head = table.header.map((cell) => style(`**${cell}**`, undefined))
147
+ const digits = Math.max(...table.rows.map((row) => String(row.number).length))
148
+ const rows = table.rows.map((row) => {
149
+ const numbered = NUMBER.test(row.question)
150
+ return {
151
+ lead: numbered ? digits - String(row.number).length : 0,
152
+ indent: numbered ? digits + 3 : 0,
153
+ question: style(row.question, marked === row.number ? PICKED : undefined),
154
+ answers: row.answers.map((answer) =>
155
+ style(answer.text, chosen?.get(row.number) === answer.letter ? PICKED : undefined))
156
+ }
157
+ })
158
+ const w1nat = Math.max(width(head[0]!), ...rows.map((row) => row.lead + width(row.question)))
159
+ const w2nat = Math.max(width(head[1]!), ...rows.flatMap((row) => row.answers.map((answer) => width(answer))))
160
+ let w1 = Math.min(w1nat, Math.floor(avail * 0.4))
161
+ const w2 = Math.min(w2nat, avail - w1)
162
+ w1 = Math.min(w1nat, avail - w2)
163
+ return { head, rows, w1, w2 }
164
+ }
165
+
166
+ /* the grill table: its state and its operations */
167
+ export const modGrill = () => {
168
+ /* the last known width of the terminal, the already seen tables, the
169
+ latest (still unanswered) table, the picks and the selected question of the
170
+ still unsubmitted prompt, and the frozen picks of the already answered tables */
171
+ let columns = 80
172
+ const seen = new Set<string>()
173
+ let latest: string | undefined
174
+ let picks = new Map<number, string>()
175
+ let selected: number | undefined
176
+ const frozen = new Map<string, Map<number, string>>()
177
+
178
+ /* refresh the picks and the selection from the prompt */
179
+ const refresh = (text: string) => {
180
+ picks = pick(text)
181
+ selected = select(text)
182
+ }
183
+
184
+ /* track the picks inside the edited prompt (and tell whether they changed) */
185
+ const track = (text: string): boolean => {
186
+ const before = JSON.stringify([ [ ...picks ], selected ])
187
+ refresh(text)
188
+ return JSON.stringify([ [ ...picks ], selected ]) !== before
189
+ }
190
+
191
+ /* freeze the picks in the latest table once the prompt is submitted */
192
+ const submit = (text: string) => {
193
+ if (latest !== undefined)
194
+ frozen.set(latest, pick(text))
195
+ latest = undefined
196
+ picks = new Map()
197
+ selected = undefined
198
+ }
199
+
200
+ /* insert the number of a clicked question or the short response of a clicked
201
+ answer into the prompt and refresh the picks and the selection from it */
202
+ const press = (engine: Pick<Engine, "invalidate" | "prompt" | "fill">,
203
+ number: number, letter?: string) => () => {
204
+ engine.prompt()
205
+ .then(({ text }) => engine.fill(insert(text, number, letter)))
206
+ .then((box) => {
207
+ refresh(box.text)
208
+ engine.invalidate()
209
+ })
210
+ .catch(() => {
211
+ /* ignore a prompt which is no longer accessible */
212
+ })
213
+ }
214
+
215
+ /* redraw a reply which contains a grilling table (terminal only),
216
+ resp. draw nothing for any other reply */
217
+ const render = (e: Frozen<MatchedEvent<"ui.render", { component: "AssistantMessage", surface: "terminal" }>>,
218
+ ui: Elements["terminal"], engine: Pick<Engine, "invalidate" | "prompt" | "fill">): RenderElement | undefined => {
219
+ /* act on grilling tables only, which fit into the terminal
220
+ (gutter, grid lines, cell paddings, and a spare column subtracted) */
221
+ columns = e.viewport?.columns ?? columns
222
+ const table = parse(e.props.text)
223
+ const avail = columns - 2 - 7 - 1
224
+ if (table === undefined || avail < 40 || table.before.length > LIMIT || table.after.length > LIMIT)
225
+ return undefined
226
+
227
+ /* determine the picks: frozen ones or, for the latest table, the live ones */
228
+ if (!seen.has(e.requestId)) {
229
+ seen.add(e.requestId)
230
+ latest = e.requestId
231
+ }
232
+ const chosen = frozen.get(e.requestId) ?? (e.requestId === latest ? picks : undefined)
233
+ const marked = e.requestId === latest ? selected : undefined
234
+
235
+ /* style the cells and determine the column widths */
236
+ const { head, rows, w1, w2 } = layout(table, avail, chosen, marked)
237
+
238
+ /* resolve the UI elements */
239
+ const { Box, Text, Button, Markdown } = ui
240
+
241
+ /* draw a line of a cell: its styled runs, padded to the column width */
242
+ const padded = (chars: Char[], span: number): (string | RenderElement)[] =>
243
+ [ ...paint(Text, chars), " ".repeat(Math.max(0, span - width(chars))) ]
244
+
245
+ /* draw a line of a cell: in the latest table as the styled text inside a
246
+ hover-scoping box, which while any line of its group (all lines of the
247
+ same question or answer) is hovered reveals a row of clickable plain
248
+ buttons (one per styled run, inverted in the default colors and carrying
249
+ the run's bold style, but not its color, as its hover style, because the
250
+ engine leaves the space cells uncolored) on top of it; otherwise as
251
+ styled text only */
252
+ const draw = (chars: Char[], span: number, group: string, line: number,
253
+ onPress: (() => void) | undefined): RenderElement[] => {
254
+ const w = width(chars)
255
+ if (e.requestId !== latest || w === 0 || onPress === undefined)
256
+ return [ Text({ children: padded(chars, span) }) ]
257
+ const buttons = split(chars).map(({ text, bold }, r) =>
258
+ Button({ key: `${group}/${line}/${r}`, label: text, plain: true, onPress,
259
+ hover: { scope: group, inverse: true, bold } }))
260
+ return [
261
+ Box({ key: `${group}/${line}`, hover: { scope: group }, children: [
262
+ Text({ children: padded(chars, w) }),
263
+ Box({ position: "absolute", top: 0, left: 0, display: "none",
264
+ hover: { scope: group, display: "flex" }, flexDirection: "row", children: buttons })
265
+ ] }),
266
+ Text({ children: " ".repeat(Math.max(0, span - w)) })
267
+ ]
268
+ }
269
+
270
+ /* draw the table as a grid: a rule between all rows and one answer per line */
271
+ const rule = (l: string, m: string, r: string) =>
272
+ Text({ wrap: "truncate-end", children: l + "─".repeat(w1 + 2) + m + "─".repeat(w2 + 2) + r })
273
+ const grid: RenderElement[] = [
274
+ rule("┌", "┬", "┐"),
275
+ Text({ wrap: "truncate-end", children:
276
+ [ "│ ", ...padded(head[0]!, w1), " │ ", ...padded(head[1]!, w2), " │" ] }),
277
+ rule("├", "┼", "┤")
278
+ ]
279
+ rows.forEach((row, k) => {
280
+ const number = table.rows[k]!.number
281
+ const q = wrap(row.question, w1, row.indent, row.lead).map((line, i) =>
282
+ draw(line, w1, `q${number}`, i, press(engine, number)))
283
+ const a = row.answers.flatMap((answer, j) => {
284
+ const letter = table.rows[k]!.answers[j]!.letter
285
+ return wrap(answer, w2, INDENT).map((line, i) =>
286
+ draw(line, w2, `a${number}${letter}`, i, letter !== "" ? press(engine, number, letter) : undefined))
287
+ })
288
+ for (let i = 0; i < Math.max(q.length, a.length); i++)
289
+ grid.push(Box({ flexDirection: "row", children: [
290
+ Text({ children: "│ " }),
291
+ ...(q[i] ?? [ Text({ children: " ".repeat(w1) }) ]),
292
+ Text({ children: " │ " }),
293
+ ...(a[i] ?? [ Text({ children: " ".repeat(w2) }) ]),
294
+ Text({ children: " │" })
295
+ ] }))
296
+ grid.push(k < rows.length - 1 ? rule("├", "┼", "┤") : rule("└", "┴", "┘"))
297
+ })
298
+
299
+ /* mimic the reply row of the engine: blank row above and bullet gutter */
300
+ return Box({ flexDirection: "row", marginTop: 1, width: "100%", children: [
301
+ Box({ flexShrink: 0, minWidth: 2, children: Text({ children: e.props.isFirstOfReply ? "⏺" : " " }) }),
302
+ Box({ flexDirection: "column", flexGrow: 1, flexShrink: 1, children: [
303
+ ...(table.before !== "" ? [ Markdown({ text: table.before }) ] : []),
304
+ Box({ flexDirection: "column", marginTop: table.before !== "" ? 1 : 0, children: grid }),
305
+ ...(table.after !== "" ? [ Box({ marginTop: 1, children: Markdown({ text: table.after }) }) ] : [])
306
+ ] })
307
+ ] })
308
+ }
309
+
310
+ return { refresh, track, submit, render }
311
+ }
312
+