@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,444 @@
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
+ Common parts, shared by the grill table, the intent help, the synopsis
9
+ form, the manual band, the manpage browser, and the hint band: the styled
10
+ characters of the terminal drawing, their Markdown conversion, wrapping,
11
+ and framing, and the contract, keys, geometry, and drawing of the
12
+ master/detail view in the band above the prompt. */
13
+
14
+ import type { Elements, Frozen, MatchedEvent, ModelCompleteRequest, ModelForkResult,
15
+ PromptBox, PromptEditInput, PromptFilled, RenderElement } from "claude-code"
16
+
17
+ /* the services of the engine, bound by the hooking infrastructure for the
18
+ modules (as the engine refuses to load a module passing "$" itself): the
19
+ redrawing, the reading of a plugin file and of the names of the directories
20
+ in a plugin directory, the forked and the plain model completion, the
21
+ waiting, and the reading and filling of the prompt */
22
+ export type Engine = {
23
+ invalidate: () => void,
24
+ read: (file: string) => Promise<string>,
25
+ dirs: (dir: string) => Promise<string[]>,
26
+ fork: (prompt: string) => Promise<ModelForkResult | null>,
27
+ complete: (request: ModelCompleteRequest) => Promise<string>,
28
+ sleep: (ms: number) => Promise<void>,
29
+ prompt: () => Promise<PromptBox>,
30
+ fill: (text: string) => Promise<PromptFilled>
31
+ }
32
+
33
+ /* the colors of a picked (resp. selected or focused) item, of a code span,
34
+ and of a skill command (the theme color of a command inside the prompt) */
35
+ export const PICKED = "red"
36
+ export const CODE = "blue"
37
+ export const COMMAND = "suggestion"
38
+
39
+ /* the trigger suffix of a synopsis form or an intent ("!!"), resp. of a manual band
40
+ or a manpage browser ("//") inside the prompt (each further constrained by the
41
+ pattern of the synopsis form, the intent, the manual band, resp. the browser) */
42
+ export const TRIGGER = /(?:!!|\/\/)$/
43
+
44
+ /* the share of the terminal height taken by the band above the prompt */
45
+ const BAND = 0.25
46
+
47
+ /* the self-drawn rounded frame around the content of the band above the
48
+ prompt: the columns taken by its borders and its left padding, the
49
+ columns before its content, and the rows taken by its edges, and the
50
+ style of its lines (the dimmed default color, which renders as a darker
51
+ grey than the "gray" color) */
52
+ const FRAME = { cols: 3, lead: 2, rows: 2 } as const
53
+ const BORDER = { dimColor: true } as const
54
+
55
+ /* the events of drawing resp. scrolling the band above the prompt */
56
+ export type BandRender = Frozen<MatchedEvent<"ui.render", { component: "AbovePrompt", surface: "terminal" }>>
57
+ export type BandScroll = Frozen<MatchedEvent<"ui.scroll", { component: "AbovePrompt" }>>
58
+
59
+ /* a band above the prompt (the synopsis form, the intent band, or the manpage
60
+ browser, of which at most one is shown at a time), as dispatched by the
61
+ hooking infrastructure: whether it is shown, closing it, closing it on a
62
+ failure (determining the prompt to restore), operating it by a key
63
+ (determining the prompt to hold, resp. the text onto whose end the edit is
64
+ still to be applied), by the wheel, and by RETURN at the prompt
65
+ (determining the prompt to refill), and drawing it */
66
+ export type Band = {
67
+ active: () => boolean,
68
+ reset: () => void,
69
+ cancel: () => string | undefined,
70
+ edit: (e: Frozen<PromptEditInput>) => PromptBox | string,
71
+ scroll: (e: BandScroll) => boolean,
72
+ submit: () => string,
73
+ render: (e: BandRender, ui: Elements["terminal"]) => RenderElement | undefined
74
+ }
75
+
76
+ /* hold the prompt while the band above the prompt is shown: the text with a
77
+ trailing space and backslash before the cursor and another space after it
78
+ (so, as a move or deletion at either end of the prompt raises no edit, moves
79
+ and deletions in both directions stay possible, and, as RETURN after a
80
+ backslash inserts a newline instead of submitting the prompt, RETURN
81
+ arrives as an edit, too, which keeps the cursor inside the prompt) */
82
+ export const held = (text: string): PromptBox =>
83
+ ({ text: `${text} \\ `, cursor: text.length + 2 })
84
+
85
+ /* a key pressed in the band above the prompt: its name, its typed text, its
86
+ cursor step (-1 left, +1 right), and its deletion (-1 backward, +1 forward) */
87
+ export type Key = { name: string, text: string, step: number, erase: number }
88
+
89
+ /* decode an edit of the held prompt into a key, where RETURN arrives as a
90
+ typed newline, and cursor moves and deletions are told by their splice */
91
+ export const decode = (e: Frozen<PromptEditInput>): Key => {
92
+ const name = e.inputText === "\n" ? "return" : e.key?.key ?? ""
93
+ const erased = e.inputText === "" && e.end > e.start
94
+ return {
95
+ name,
96
+ text: e.inputText === "\n" ? "" : e.inputText,
97
+ step: name === "left" ? -1 : name === "right" ? 1 :
98
+ e.inputText === "" && !erased ? Math.sign(e.start - e.cursor) : 0,
99
+ erase: erased ? (e.start < e.cursor ? -1 : 1) : 0
100
+ }
101
+ }
102
+
103
+ /* a single styled character of the terminal drawing */
104
+ export type Char = { ch: string, bold: boolean, color: string | undefined }
105
+
106
+ /* extract a section of a manual page (without its heading) */
107
+ export const section = (manual: string, name: string): string =>
108
+ manual.split(/^(?=## {2})/m).find((text) => text.startsWith(`## ${name}`))?.replace(/^.*\n/, "") ?? ""
109
+
110
+ /* convert inline Markdown (bold and code spans) into styled characters */
111
+ export const style = (text: string, color: string | undefined): Char[] => {
112
+ const chars: Char[] = []
113
+ let bold = false
114
+ let code = false
115
+ for (let i = 0; i < text.length; i++) {
116
+ if (!code && text.startsWith("**", i)) {
117
+ bold = !bold
118
+ i++
119
+ }
120
+ else if (text[i] === "`")
121
+ code = !code
122
+ else {
123
+ const ch = String.fromCodePoint(text.codePointAt(i)!)
124
+ chars.push({ ch, bold, color: color ?? (code ? CODE : undefined) })
125
+ i += ch.length - 1
126
+ }
127
+ }
128
+ return chars
129
+ }
130
+
131
+ /* the width of styled characters in terminal cells */
132
+ export const width = (chars: Char[]): number => {
133
+ let n = 0
134
+ for (const { ch } of chars) {
135
+ const c = ch.codePointAt(0)!
136
+ if ((c >= 0x200B && c <= 0x200D) || c === 0x2060 || c === 0xFEFF ||
137
+ (c >= 0x0300 && c <= 0x036F) || (c >= 0xFE00 && c <= 0xFE0F))
138
+ continue
139
+ const wide =
140
+ (c >= 0x1100 && c <= 0x115F) || (c >= 0x2E80 && c <= 0xA4CF) ||
141
+ (c >= 0xAC00 && c <= 0xD7A3) || (c >= 0xF900 && c <= 0xFAFF) ||
142
+ (c >= 0xFF00 && c <= 0xFF60) || (c >= 0x1F300 && c <= 0x1FAFF) ||
143
+ (c >= 0x20000 && c <= 0x3FFFD)
144
+ n += wide ? 2 : 1
145
+ }
146
+ return n
147
+ }
148
+
149
+ /* hard-wrap styled characters at word boundaries to the given columns,
150
+ indenting the first line by "lead" and all further lines by "indent"
151
+ (and chopping overlong words) */
152
+ export const wrap = (chars: Char[], columns: number, indent: number, lead = 0): Char[][] => {
153
+ const space: Char = { ch: " ", bold: false, color: undefined }
154
+ const words: Char[][] = [ [] ]
155
+ for (const char of chars) {
156
+ if (char.ch === " ")
157
+ words.push([])
158
+ else
159
+ words.at(-1)!.push(char)
160
+ }
161
+ const lines: Char[][] = []
162
+ let line: Char[] = Array.from({ length: lead }, () => space)
163
+ let start = lead
164
+ let size = lead
165
+ const flush = () => {
166
+ lines.push(line)
167
+ line = Array.from({ length: indent }, () => space)
168
+ start = indent
169
+ size = indent
170
+ }
171
+ for (let word of words.filter((word) => word.length > 0)) {
172
+ while (word.length > 0) {
173
+ const w = width(word)
174
+ if (size > start && size + 1 + w <= columns) {
175
+ line.push(space, ...word)
176
+ size += 1 + w
177
+ word = []
178
+ }
179
+ else if (size > start)
180
+ flush()
181
+ else {
182
+ let n = 1
183
+ while (n < word.length && width(word.slice(0, n + 1)) <= columns - start)
184
+ n++
185
+ line.push(...word.slice(0, n))
186
+ size += width(word.slice(0, n))
187
+ word = word.slice(n)
188
+ if (word.length > 0)
189
+ flush()
190
+ }
191
+ }
192
+ }
193
+ lines.push(line)
194
+ return lines
195
+ }
196
+
197
+ /* a run of equally styled characters */
198
+ type Run = { text: string, bold: boolean, color: string | undefined }
199
+
200
+ /* split a line of styled characters into runs of equally styled characters */
201
+ export const split = (chars: Char[]): Run[] => {
202
+ const runs: Run[] = []
203
+ for (let i = 0, j = 0; i < chars.length; i = j) {
204
+ const { bold, color } = chars[i]!
205
+ while (j < chars.length && chars[j]!.bold === bold && chars[j]!.color === color)
206
+ j++
207
+ runs.push({ text: chars.slice(i, j).map((char) => char.ch).join(""), bold, color })
208
+ }
209
+ return runs
210
+ }
211
+
212
+ /* convert plain text (without any Markdown) into equally styled characters */
213
+ export const uniform = (text: string, bold: boolean, color: string | undefined): Char[] =>
214
+ Array.from(text, (ch) => ({ ch, bold, color }))
215
+
216
+ /* strip the single-asterisk emphasis outside the code spans of inline Markdown */
217
+ const strip = (text: string): string =>
218
+ text.split("`").map((part, i) => i % 2 === 0 ? part.replace(/(?<!\*)\*(?!\*)/g, "") : part).join("`")
219
+
220
+ /* convert the Markdown blocks of a manual page section (paragraphs, list items,
221
+ and code fences) into lines of styled characters, hard-wrapped to the given
222
+ columns (except for the code lines), with a blank line between the blocks,
223
+ where "nested" keeps the indentation of the blocks and of the list item texts */
224
+ export const flow = (text: string, columns: number, nested = false): Char[][] => {
225
+ const result: Char[][] = []
226
+ let block: string[] = []
227
+ let fence = false
228
+ const flush = () => {
229
+ if (block.length === 0)
230
+ return
231
+ if (result.length > 0)
232
+ result.push([])
233
+ const pad = nested ? /^ */.exec(block[0]!)![0].length : 0
234
+ const body = strip(block.map((line) => line.trim()).join(" "))
235
+ const item = /^(-|\d+\.)\s+/.exec(body)
236
+ if (item !== null) {
237
+ const mark = (item[1] === "-" ? "•" : item[1]!).padEnd(nested ? item[0].length : item[1]!.length + 1)
238
+ const lines = wrap(style(body.slice(item[0].length), undefined), columns, pad + mark.length, pad + mark.length)
239
+ lines[0]!.splice(pad, mark.length, ...uniform(mark, false, undefined))
240
+ result.push(...lines)
241
+ }
242
+ else
243
+ result.push(...wrap(style(body, undefined), columns, pad, pad))
244
+ block = []
245
+ }
246
+ for (const line of text.replace(/^\s*\n|\s+$/g, "").split("\n")) {
247
+ if (line.startsWith("```")) {
248
+ flush()
249
+ if (!fence && result.length > 0)
250
+ result.push([])
251
+ fence = !fence
252
+ }
253
+ else if (fence)
254
+ result.push(uniform(line, false, CODE))
255
+ else if (line.trim() === "")
256
+ flush()
257
+ else {
258
+ if (/^(?:-|\d+\.)\s/.test(line))
259
+ flush()
260
+ block.push(line)
261
+ }
262
+ }
263
+ flush()
264
+ return result
265
+ }
266
+
267
+ /* surround lines of styled characters with a rounded box of the given total
268
+ columns and border color (padding the lines to the inner columns) */
269
+ export const frame = (lines: Char[][], columns: number, color: string | undefined): Char[][] => {
270
+ const rim = (text: string) => uniform(text, false, color)
271
+ return [
272
+ rim(`╭${"─".repeat(columns - 2)}╮`),
273
+ ...lines.map((line) => [ ...rim("│ "), ...line,
274
+ ...uniform(" ".repeat(Math.max(0, columns - 4 - width(line))), false, undefined), ...rim(" │") ]),
275
+ rim(`╰${"─".repeat(columns - 2)}╯`)
276
+ ]
277
+ }
278
+
279
+ /* split a horizontal edge of a frame into runs with a title placed after its
280
+ first and another one before its last two characters (each if it fits, the
281
+ one at the end with precedence), flagged whether they are a title */
282
+ const titled = (line: string, head: string, tail: string): [ string, boolean ][] => {
283
+ const end = tail !== "" && line.length >= tail.length + 4 ? tail : ""
284
+ const start = head !== "" && line.length >= head.length + (end !== "" ? end.length + 6 : 4) ? head : ""
285
+ const k = end !== "" ? line.length - 2 - end.length : line.length
286
+ return [
287
+ ...(start !== "" ? [ [ line.slice(0, 2), false ], [ start, true ] ] as [ string, boolean ][] : []),
288
+ [ line.slice(start !== "" ? 2 + start.length : 0, k), false ],
289
+ ...(end !== "" ? [ [ end, true ], [ line.slice(k + end.length), false ] ] as [ string, boolean ][] : [])
290
+ ]
291
+ }
292
+
293
+ /* the scroll position of a view with the given rows over the given lines
294
+ at the given scroll position, as placed into the bottom edge of a frame */
295
+ const position = (rows: number, lines: number, scroll: number): string =>
296
+ lines === 0 ? "" : `[ lines ${scroll + 1}-${Math.min(scroll + rows, lines)}/${lines} ]`
297
+
298
+ /* the mark of a scrollbar row: the thumb, the track, or nothing */
299
+ type Mark = "thumb" | "track" | "none"
300
+
301
+ /* determine the scrollbar of a view with the given rows over the given lines at
302
+ the given scroll position: for each row the thumb, the track, or nothing */
303
+ const scrollbar = (rows: number, lines: number, scroll: number): Mark[] => {
304
+ if (lines <= rows)
305
+ return Array.from({ length: rows }, () => "none")
306
+ const thumb = Math.max(1, Math.round(rows * rows / lines))
307
+ const offset = Math.round(scroll / (lines - rows) * (rows - thumb))
308
+ return Array.from({ length: rows }, (_, i) => i >= offset && i < offset + thumb ? "thumb" : "track")
309
+ }
310
+
311
+ /* determine the rows a scroll event moves a view by: a page of the given size
312
+ for the scroll keys (PAGEUP/PAGEDOWN), resp. its rows for the wheel */
313
+ const paging = (e: { pointer?: unknown, by: number }, size: number): number =>
314
+ e.pointer === undefined && Math.abs(e.by) > 1 ? Math.sign(e.by) * size : e.by
315
+
316
+ /* the text element of the terminal surface */
317
+ export type Text = Elements["terminal"]["Text"]
318
+
319
+ /* draw a line of styled characters: its styled runs */
320
+ export const paint = (Text: Text, chars: Char[]): (string | RenderElement)[] =>
321
+ split(chars).map(({ text, bold, color }) =>
322
+ bold || color !== undefined ? Text({ bold, color, children: text }) : text)
323
+
324
+ /* draw a horizontal edge of the self-drawn frame (dimmed), with titles at its
325
+ start and its end (with their brackets dimmed, an "ASE" prefix before a colon
326
+ in bold, and the rest dimmed) */
327
+ const edge = (Text: Text, line: string, head = "", tail = ""): RenderElement =>
328
+ Text({ children: titled(line, head, tail).flatMap(([ text, label ]) => {
329
+ const m = /^(\[ )?([^:]*)(:.*?)( \])?$/.exec(text)
330
+ if (!label || m === null)
331
+ return [ Text({ ...BORDER, children: text }) ]
332
+ return [
333
+ ...(m[1] !== undefined ? [ Text({ ...BORDER, children: m[1] }) ] : []),
334
+ Text({ bold: true, children: m[2]! }),
335
+ Text({ dimColor: true, children: m[3]! }),
336
+ ...(m[4] !== undefined ? [ Text({ ...BORDER, children: m[4] }) ] : [])
337
+ ]
338
+ }) })
339
+
340
+ /* draw a row of a scrollbar: the thumb, the (dimmed) track, or nothing */
341
+ const bar = (Text: Text, mark: Mark | undefined): RenderElement =>
342
+ mark === "thumb" ? Text({ children: "█" }) :
343
+ mark === "track" ? Text({ dimColor: true, children: "░" }) : Text({ children: " " })
344
+
345
+ /* the geometry of the band above the prompt: the inner columns of the self-drawn
346
+ frame, the rows of its views, the columns of its master and detail view, and
347
+ whether the band overflows (to receive the scrolling) */
348
+ export type Layout = { span: number, body: number, master: number, detail: number, overflow: boolean }
349
+
350
+ /* determine the geometry of the band above the prompt: the inner columns (both
351
+ borders and the left padding subtracted), the rows (with the frame and the footer
352
+ subtracted) out of a share of the terminal height (but never more than the rows of
353
+ the band), resp., in fullscreen (where the engine raises the scrolling by the wheel
354
+ and, once focused, by the keys only for a tree taller than the band), all rows of
355
+ the band but the one of the engine's "n more" row (as the band then takes them
356
+ anyway), and the columns of the master view (by the given share) and of the
357
+ detail view (with a separator column between them, and a gap of two columns and
358
+ a scrollbar column right of the detail view) */
359
+ export const layout = (e: BandRender, share: number, band = BAND): Layout => {
360
+ const span = Math.max(20, e.props.bodyColumns - FRAME.cols)
361
+ const overflow = e.viewport?.isFullscreen === true
362
+ const height = overflow ? Math.max(FRAME.rows + 2, e.props.maxRows - 1) : Math.min(e.props.maxRows,
363
+ Math.max(FRAME.rows + 2, Math.floor((e.viewport?.rows ?? e.props.maxRows) * band)))
364
+ const master = Math.floor((span - 3 - 3) * share)
365
+ return { span, body: Math.max(1, height - FRAME.rows - 1), master, detail: span - master - 3 - 3, overflow }
366
+ }
367
+
368
+ /* the scroll state of the band above the prompt: the first shown row of the
369
+ master view, the first shown line and the lines of a page of the detail
370
+ view, and the first band column right of the master view */
371
+ export type View = { top: number, scroll: number, page: number, split: number }
372
+
373
+ /* operate a view by the wheel: over the master view step it (if steppable),
374
+ else scroll the detail view, resp. scroll it by a page with the scroll keys
375
+ (PAGEUP/PAGEDOWN) while the band is focused */
376
+ export const wheel = (e: BandScroll, view: View, step?: (by: number) => void): void => {
377
+ if (step !== undefined && e.pointer !== undefined && e.pointer.column < view.split)
378
+ step(Math.sign(e.by))
379
+ else
380
+ view.scroll = Math.max(0, view.scroll + paging(e, view.page))
381
+ }
382
+
383
+ /* draw the band above the prompt: inside the self-drawn frame (with the title
384
+ in its top edge) either a single view of the given rows, a single view of the
385
+ given lines with its scrollbar, or a master view of the given rows (where the
386
+ given rule rows are horizontal lines joining the borders) and, joined by a
387
+ separator, a detail view of the given lines (optionally dimmed) with its
388
+ scrollbar, into the bottom edge the given status (if any) at its start and
389
+ the scroll position of the lines (unless they always fit) at its end, and below
390
+ the frame the key hints (followed by blank rows for an overflowing band),
391
+ where the scroll position is clamped, and the page size (for the scroll keys)
392
+ and the end of the master view (for the wheel) are remembered in the view */
393
+ export const panel = (ui: Elements["terminal"], { span, body, master, detail, overflow }: Layout, view: View, spec: {
394
+ title: string,
395
+ status?: string,
396
+ hints: string,
397
+ rows?: (i: number) => RenderElement | undefined,
398
+ rule?: (i: number) => boolean,
399
+ lines?: Char[][],
400
+ dim?: boolean,
401
+ fit?: boolean
402
+ }): RenderElement => {
403
+ const { Box, Text } = ui
404
+ const lines = spec.lines ?? []
405
+ const solo = spec.rows === undefined
406
+ view.page = Math.max(1, body - 1)
407
+ view.split = solo ? 0 : FRAME.lead + master
408
+ view.scroll = Math.max(0, Math.min(view.scroll, lines.length - body))
409
+ const marks = scrollbar(body, lines.length, view.scroll)
410
+ const blank = Text({ children: " " })
411
+ const border = (left: string, joint: string, right: string) =>
412
+ left + (spec.lines === undefined || solo ? "─".repeat(span + 1) :
413
+ "─".repeat(master + 2) + joint + "─".repeat(detail + 4)) + right
414
+ const scrolled = (i: number, width: number) => [
415
+ Box({ width, flexShrink: 0, children:
416
+ Text({ wrap: "truncate-end", dimColor: spec.dim ?? false,
417
+ children: paint(Text, lines[view.scroll + i] ?? []) }) }),
418
+ Text({ children: " " }),
419
+ bar(Text, marks[i])
420
+ ]
421
+ const rows: RenderElement[] = [ edge(Text, border("╭", "┬", "╮"), spec.title) ]
422
+ for (let i = 0; i < body; i++) {
423
+ const rule = !solo && spec.lines !== undefined && spec.rule?.(i) === true
424
+ rows.push(Box({ flexDirection: "row", children: [
425
+ Text({ ...BORDER, children: rule ? "├─" : "│ " }),
426
+ ...(solo ? scrolled(i, span - 3) : spec.lines === undefined ? [
427
+ Box({ width: span, flexShrink: 0, children: spec.rows!(i) ?? blank })
428
+ ] : [
429
+ rule ? Text({ ...BORDER, children: `${"─".repeat(master + 1)}┤ ` }) :
430
+ Box({ width: master, flexShrink: 0, children: spec.rows!(i) ?? blank }),
431
+ ...(rule ? [] : [ Text({ ...BORDER, children: " │ " }) ]),
432
+ ...scrolled(i, detail)
433
+ ]),
434
+ Text({ ...BORDER, children: "│" })
435
+ ] }))
436
+ }
437
+ rows.push(edge(Text, border("╰", "┴", "╯"), spec.status,
438
+ spec.fit === true ? "" : position(body, lines.length, view.scroll)))
439
+ rows.push(Box({ paddingLeft: 1, children: Text({ dimColor: true, wrap: "truncate-end", children: spec.hints }) }))
440
+ if (overflow)
441
+ rows.push(blank, blank)
442
+ return Box({ flexDirection: "column", width: "100%", children: rows })
443
+ }
444
+