@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.
- package/dst/ase-hook.js +13 -4
- package/dst/ase-lib-version.js +14 -5
- package/dst/ase-man-catalog.js +229 -0
- package/dst/ase-man-render.js +278 -0
- package/dst/ase-man-tui.js +636 -0
- package/dst/ase-man.js +33 -27
- package/dst/ase-setup.js +2 -2
- package/dst/ase-spec.js +103 -20
- package/dst/ase-task-board-filter.js +5 -4
- package/dst/ase-task-board-web-client.js +26 -26
- package/dst/man/ase-agent-adhoc.md +1165 -0
- package/dst/man/ase-agent-condenser.md +383 -0
- package/dst/man/ase-agent-editor.md +441 -0
- package/dst/man/ase-agent-estimator.md +477 -0
- package/dst/man/ase-agent-fleet.md +358 -0
- package/dst/man/ase-agent-implementer.md +521 -0
- package/dst/man/ase-agent-integrator.md +449 -0
- package/dst/man/ase-agent-qualifier.md +426 -0
- package/dst/man/ase-agent-rebooter.md +384 -0
- package/dst/man/ase-agent-relay.md +262 -0
- package/dst/man/ase-agent-researcher.md +478 -0
- package/dst/man/ase-agent.md +628 -0
- package/dst/man/ase-artifact-list.md +57 -0
- package/dst/man/ase-artifact-name.md +56 -0
- package/dst/man/ase-artifact.md +92 -0
- package/dst/man/ase-config-delete.md +74 -0
- package/dst/man/ase-config-edit.md +83 -0
- package/dst/man/ase-config-get.md +70 -0
- package/dst/man/ase-config-init.md +110 -0
- package/dst/man/ase-config-list.md +68 -0
- package/dst/man/ase-config-set.md +92 -0
- package/dst/man/ase-config.md +426 -0
- package/dst/man/ase-hook-permission-request.md +61 -0
- package/dst/man/ase-hook-pre-tool-use.md +96 -0
- package/dst/man/ase-hook-session-end.md +71 -0
- package/dst/man/ase-hook-session-persona.md +65 -0
- package/dst/man/ase-hook-session-start.md +184 -0
- package/dst/man/ase-hook-stop.md +52 -0
- package/dst/man/ase-hook-user-prompt-submit.md +54 -0
- package/dst/man/ase-hook.md +93 -0
- package/dst/man/ase-man.md +195 -0
- package/dst/man/ase-mcp.md +102 -0
- package/dst/man/ase-service-log.md +59 -0
- package/dst/man/ase-service-send.md +60 -0
- package/dst/man/ase-service-start.md +93 -0
- package/dst/man/ase-service-status.md +58 -0
- package/dst/man/ase-service-stop.md +52 -0
- package/dst/man/ase-service.md +124 -0
- package/dst/man/ase-setup-disable.md +67 -0
- package/dst/man/ase-setup-enable.md +66 -0
- package/dst/man/ase-setup-install.md +116 -0
- package/dst/man/ase-setup-mcp.md +213 -0
- package/dst/man/ase-setup-status.md +101 -0
- package/dst/man/ase-setup-statusline.md +162 -0
- package/dst/man/ase-setup-telemetry.md +166 -0
- package/dst/man/ase-setup-uninstall.md +85 -0
- package/dst/man/ase-setup-update.md +117 -0
- package/dst/man/ase-setup.md +156 -0
- package/dst/man/ase-spec-export.md +99 -0
- package/dst/man/ase-spec-init.md +57 -0
- package/dst/man/ase-spec-lint.md +74 -0
- package/dst/man/ase-spec-preview.md +57 -0
- package/dst/man/ase-spec.md +85 -0
- package/dst/man/ase-statusline.md +330 -0
- package/dst/man/ase-task-assign.md +138 -0
- package/dst/man/ase-task-board.md +651 -0
- package/dst/man/ase-task-delete.md +46 -0
- package/dst/man/ase-task-edit.md +73 -0
- package/dst/man/ase-task-idscheme.md +92 -0
- package/dst/man/ase-task-lifecycle.md +92 -0
- package/dst/man/ase-task-list.md +86 -0
- package/dst/man/ase-task-load.md +48 -0
- package/dst/man/ase-task-newid.md +100 -0
- package/dst/man/ase-task-purge.md +49 -0
- package/dst/man/ase-task-rename.md +69 -0
- package/dst/man/ase-task-request.md +108 -0
- package/dst/man/ase-task-save.md +78 -0
- package/dst/man/ase-task-status.md +79 -0
- package/dst/man/ase-task-store.md +377 -0
- package/dst/man/ase-task-view.md +50 -0
- package/dst/man/ase-task.md +335 -0
- package/dst/man/ase-util-compat.md +38 -0
- package/dst/man/ase-util-diagram.md +140 -0
- package/dst/man/ase-util-meta.md +68 -0
- package/dst/man/ase-util-metric.md +74 -0
- package/dst/man/ase-util-mint.md +85 -0
- package/dst/man/ase-util-worktree.md +109 -0
- package/dst/man/ase-util.md +81 -0
- package/dst/man/ase.md +228 -0
- package/package.json +16 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-code-lint.md +3 -3
- package/plugin/agents/ase-docs-proofread.md +3 -3
- package/plugin/agents/ase-docs-refine.md +3 -3
- package/plugin/agents/ase-spec-lint.md +338 -0
- package/plugin/mods/ase-mods-common.ts +444 -0
- package/plugin/mods/ase-mods-grill.ts +312 -0
- package/plugin/mods/ase-mods-help-browse.ts +405 -0
- package/plugin/mods/ase-mods-help-compose.ts +561 -0
- package/plugin/mods/ase-mods-help-find.ts +426 -0
- package/plugin/mods/ase-mods-help-hint.ts +134 -0
- package/plugin/mods/ase-mods-help-show.ts +142 -0
- package/plugin/mods/ase-mods.ts +214 -381
- package/plugin/mods/claude-code.d.ts +26 -3
- package/plugin/skills/ase-code-lint/SKILL.md +15 -15
- package/plugin/skills/ase-code-lint/help.md +21 -8
- package/plugin/skills/ase-help-intent/SKILL.md +2 -1
- package/plugin/skills/ase-help-intent/data.md +214 -9
- package/plugin/skills/ase-help-skill/catalog.md +1 -0
- package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
- package/plugin/skills/ase-repo-commit/SKILL.md +1 -0
- package/plugin/skills/ase-repo-merge/SKILL.md +1 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +3 -3
- package/plugin/skills/ase-spec-edit/help.md +1 -1
- package/plugin/skills/ase-spec-lint/SKILL.md +695 -0
- package/plugin/skills/ase-spec-lint/help.md +184 -0
- package/plugin/skills/ase-task-implement/SKILL.md +24 -6
- package/plugin/skills/ase-task-implement/help.md +11 -1
- package/dst/man/ase-agent-adhoc.1 +0 -515
- package/dst/man/ase-agent-condenser.1 +0 -193
- package/dst/man/ase-agent-editor.1 +0 -212
- package/dst/man/ase-agent-estimator.1 +0 -214
- package/dst/man/ase-agent-fleet.1 +0 -185
- package/dst/man/ase-agent-implementer.1 +0 -233
- package/dst/man/ase-agent-integrator.1 +0 -213
- package/dst/man/ase-agent-qualifier.1 +0 -214
- package/dst/man/ase-agent-rebooter.1 +0 -193
- package/dst/man/ase-agent-relay.1 +0 -128
- package/dst/man/ase-agent-researcher.1 +0 -217
- package/dst/man/ase-agent.1 +0 -264
- package/dst/man/ase-artifact-list.1 +0 -38
- package/dst/man/ase-artifact-name.1 +0 -39
- package/dst/man/ase-artifact.1 +0 -56
- package/dst/man/ase-config-delete.1 +0 -45
- package/dst/man/ase-config-edit.1 +0 -48
- package/dst/man/ase-config-get.1 +0 -46
- package/dst/man/ase-config-init.1 +0 -60
- package/dst/man/ase-config-list.1 +0 -42
- package/dst/man/ase-config-set.1 +0 -52
- package/dst/man/ase-config.1 +0 -231
- package/dst/man/ase-hook-permission-request.1 +0 -36
- package/dst/man/ase-hook-pre-tool-use.1 +0 -59
- package/dst/man/ase-hook-session-end.1 +0 -47
- package/dst/man/ase-hook-session-persona.1 +0 -37
- package/dst/man/ase-hook-session-start.1 +0 -81
- package/dst/man/ase-hook-stop.1 +0 -34
- package/dst/man/ase-hook-user-prompt-submit.1 +0 -34
- package/dst/man/ase-hook.1 +0 -51
- package/dst/man/ase-man.1 +0 -67
- package/dst/man/ase-mcp.1 +0 -61
- package/dst/man/ase-service-log.1 +0 -43
- package/dst/man/ase-service-send.1 +0 -47
- package/dst/man/ase-service-start.1 +0 -54
- package/dst/man/ase-service-status.1 +0 -46
- package/dst/man/ase-service-stop.1 +0 -37
- package/dst/man/ase-service.1 +0 -85
- package/dst/man/ase-setup-disable.1 +0 -44
- package/dst/man/ase-setup-enable.1 +0 -45
- package/dst/man/ase-setup-install.1 +0 -73
- package/dst/man/ase-setup-mcp.1 +0 -124
- package/dst/man/ase-setup-status.1 +0 -60
- package/dst/man/ase-setup-statusline.1 +0 -92
- package/dst/man/ase-setup-telemetry.1 +0 -92
- package/dst/man/ase-setup-uninstall.1 +0 -59
- package/dst/man/ase-setup-update.1 +0 -64
- package/dst/man/ase-setup.1 +0 -93
- package/dst/man/ase-spec-export.1 +0 -62
- package/dst/man/ase-spec-lint.1 +0 -40
- package/dst/man/ase-spec-preview.1 +0 -40
- package/dst/man/ase-spec.1 +0 -46
- package/dst/man/ase-statusline.1 +0 -189
- package/dst/man/ase-task-assign.1 +0 -71
- package/dst/man/ase-task-board.1 +0 -384
- package/dst/man/ase-task-delete.1 +0 -35
- package/dst/man/ase-task-edit.1 +0 -46
- package/dst/man/ase-task-idscheme.1 +0 -65
- package/dst/man/ase-task-lifecycle.1 +0 -57
- package/dst/man/ase-task-list.1 +0 -47
- package/dst/man/ase-task-load.1 +0 -36
- package/dst/man/ase-task-newid.1 +0 -61
- package/dst/man/ase-task-purge.1 +0 -37
- package/dst/man/ase-task-rename.1 +0 -44
- package/dst/man/ase-task-request.1 +0 -61
- package/dst/man/ase-task-save.1 +0 -48
- package/dst/man/ase-task-status.1 +0 -55
- package/dst/man/ase-task-store.1 +0 -194
- package/dst/man/ase-task-view.1 +0 -40
- package/dst/man/ase-task.1 +0 -160
- package/dst/man/ase-util-compat.1 +0 -25
- package/dst/man/ase-util-diagram.1 +0 -76
- package/dst/man/ase-util-meta.1 +0 -45
- package/dst/man/ase-util-metric.1 +0 -54
- package/dst/man/ase-util-mint.1 +0 -59
- package/dst/man/ase-util-worktree.1 +0 -72
- package/dst/man/ase-util.1 +0 -50
- 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
|
+
|