@rse/ase 2.0.4 → 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 +12 -3
- package/package.json +5 -5
- 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/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/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 +42 -16
- package/plugin/skills/ase-spec-lint/SKILL.md +15 -15
- package/plugin/skills/ase-spec-lint/help.md +21 -8
package/plugin/mods/ase-mods.ts
CHANGED
|
@@ -5,227 +5,23 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/* Claude Code function hooks module ("mod", EARLY ACCESS):
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 last bare number
|
|
128
|
-
of another question (the selected one) is replaced in place, else "n" is
|
|
129
|
-
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
|
-
const last = letter === undefined ? [ ...text.matchAll(SELECT) ].at(-1) : undefined
|
|
139
|
-
if (last !== undefined)
|
|
140
|
-
return text.slice(0, last.index) + String(number) + text.slice(last.index + last[0].length)
|
|
141
|
-
const head = text.trimEnd()
|
|
142
|
-
return `${head}${head !== "" ? " " : ""}${number}${letter ?? ""}`
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/* convert inline Markdown (bold and code spans) into styled characters */
|
|
146
|
-
const style = (text: string, color: string | undefined): Char[] => {
|
|
147
|
-
const chars: Char[] = []
|
|
148
|
-
let bold = false
|
|
149
|
-
let code = false
|
|
150
|
-
for (let i = 0; i < text.length; i++) {
|
|
151
|
-
if (!code && text.startsWith("**", i)) {
|
|
152
|
-
bold = !bold
|
|
153
|
-
i++
|
|
154
|
-
}
|
|
155
|
-
else if (text[i] === "`")
|
|
156
|
-
code = !code
|
|
157
|
-
else {
|
|
158
|
-
const ch = String.fromCodePoint(text.codePointAt(i)!)
|
|
159
|
-
chars.push({ ch, bold, color: color ?? (code ? CODE : undefined) })
|
|
160
|
-
i += ch.length - 1
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return chars
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/* the width of styled characters in terminal cells */
|
|
167
|
-
const width = (chars: Char[]): number => {
|
|
168
|
-
let n = 0
|
|
169
|
-
for (const { ch } of chars) {
|
|
170
|
-
const c = ch.codePointAt(0)!
|
|
171
|
-
if (c === 0x200B || c === 0x2060 || (c >= 0x0300 && c <= 0x036F) || (c >= 0xFE00 && c <= 0xFE0F))
|
|
172
|
-
continue
|
|
173
|
-
const wide =
|
|
174
|
-
(c >= 0x1100 && c <= 0x115F) || (c >= 0x2E80 && c <= 0xA4CF) ||
|
|
175
|
-
(c >= 0xAC00 && c <= 0xD7A3) || (c >= 0xF900 && c <= 0xFAFF) ||
|
|
176
|
-
(c >= 0xFF00 && c <= 0xFF60) || (c >= 0x1F300 && c <= 0x1FAFF)
|
|
177
|
-
n += wide ? 2 : 1
|
|
178
|
-
}
|
|
179
|
-
return n
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/* hard-wrap styled characters at word boundaries to the given columns,
|
|
183
|
-
indenting the first line by "lead" and all further lines by "indent"
|
|
184
|
-
(and chopping overlong words) */
|
|
185
|
-
const wrap = (chars: Char[], columns: number, indent: number, lead = 0): Char[][] => {
|
|
186
|
-
const space: Char = { ch: " ", bold: false, color: undefined }
|
|
187
|
-
const words: Char[][] = [ [] ]
|
|
188
|
-
for (const char of chars) {
|
|
189
|
-
if (char.ch === " ")
|
|
190
|
-
words.push([])
|
|
191
|
-
else
|
|
192
|
-
words.at(-1)!.push(char)
|
|
193
|
-
}
|
|
194
|
-
const lines: Char[][] = []
|
|
195
|
-
let line: Char[] = Array.from({ length: lead }, () => space)
|
|
196
|
-
let start = lead
|
|
197
|
-
let size = lead
|
|
198
|
-
const flush = () => {
|
|
199
|
-
lines.push(line)
|
|
200
|
-
line = Array.from({ length: indent }, () => space)
|
|
201
|
-
start = indent
|
|
202
|
-
size = indent
|
|
203
|
-
}
|
|
204
|
-
for (let word of words.filter((word) => word.length > 0)) {
|
|
205
|
-
while (word.length > 0) {
|
|
206
|
-
const w = width(word)
|
|
207
|
-
if (size > start && size + 1 + w <= columns) {
|
|
208
|
-
line.push(space, ...word)
|
|
209
|
-
size += 1 + w
|
|
210
|
-
word = []
|
|
211
|
-
}
|
|
212
|
-
else if (size > start)
|
|
213
|
-
flush()
|
|
214
|
-
else {
|
|
215
|
-
let n = 1
|
|
216
|
-
while (n < word.length && width(word.slice(0, n + 1)) <= columns - start)
|
|
217
|
-
n++
|
|
218
|
-
line.push(...word.slice(0, n))
|
|
219
|
-
size += width(word.slice(0, n))
|
|
220
|
-
word = word.slice(n)
|
|
221
|
-
if (word.length > 0)
|
|
222
|
-
flush()
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
lines.push(line)
|
|
227
|
-
return lines
|
|
228
|
-
}
|
|
8
|
+
Hooking infrastructure, registering the function hooks and dispatching
|
|
9
|
+
them into the grill table ("ase-mods-grill.ts"), the intent help
|
|
10
|
+
("ase-mods-help-find.ts"), the synopsis form ("ase-mods-help-compose.ts"),
|
|
11
|
+
the manual band ("ase-mods-help-show.ts"), the manpage browser
|
|
12
|
+
("ase-mods-help-browse.ts"), and the hint band ("ase-mods-help-hint.ts").
|
|
13
|
+
As the engine loads only a module spelling "on", "$", and "next" literally,
|
|
14
|
+
this is the only module touching the engine, and it binds the engine
|
|
15
|
+
services needed by the other modules as plain functions. */
|
|
16
|
+
|
|
17
|
+
import type { Register, PromptBox, Timer } from "claude-code"
|
|
18
|
+
import { type Band, TRIGGER } from "./ase-mods-common.ts"
|
|
19
|
+
import { modGrill } from "./ase-mods-grill.ts"
|
|
20
|
+
import { modHelpFind } from "./ase-mods-help-find.ts"
|
|
21
|
+
import { modHelpCompose } from "./ase-mods-help-compose.ts"
|
|
22
|
+
import { modHelpShow } from "./ase-mods-help-show.ts"
|
|
23
|
+
import { modHelpBrowse } from "./ase-mods-help-browse.ts"
|
|
24
|
+
import { modHelpHint } from "./ase-mods-help-hint.ts"
|
|
229
25
|
|
|
230
26
|
/* function hook registration entry point */
|
|
231
27
|
export const register: Register = (on, options) => {
|
|
@@ -233,181 +29,218 @@ export const register: Register = (on, options) => {
|
|
|
233
29
|
if (options.enabled === false)
|
|
234
30
|
return
|
|
235
31
|
|
|
236
|
-
/* the
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
32
|
+
/* the grill table, the intent help, the synopsis form, the manual band,
|
|
33
|
+
the manpage browser, and the hint band */
|
|
34
|
+
const grill = modGrill()
|
|
35
|
+
const intent = modHelpFind()
|
|
36
|
+
const skill = modHelpCompose()
|
|
37
|
+
const manual = modHelpShow()
|
|
38
|
+
const browse = modHelpBrowse()
|
|
39
|
+
const hint = modHelpHint()
|
|
40
|
+
|
|
41
|
+
/* the bands above the prompt (the synopsis form, the intent band, and the
|
|
42
|
+
manpage browser, opened in this order), the shown one (as at most one is
|
|
43
|
+
shown at a time), and closing all of them resp. the shown one on a failure
|
|
44
|
+
(determining the prompt to restore) */
|
|
45
|
+
const bands: Band[] = [ skill, intent, browse ]
|
|
46
|
+
const shown = () => bands.find((band) => band.active())
|
|
47
|
+
const reset = () => bands.forEach((band) => band.reset())
|
|
48
|
+
const cancel = () => shown()?.cancel()
|
|
49
|
+
|
|
50
|
+
/* the generation of the latest submission (ending any older refilling of the prompt) */
|
|
51
|
+
let submissions = 0
|
|
52
|
+
|
|
53
|
+
/* the partial slash command inside the prompt, and the poller of the prompt
|
|
54
|
+
for the hint band while one is typed (as a completion of the typeahead
|
|
55
|
+
raises no edit) */
|
|
56
|
+
const partial = /^\/\S*$/
|
|
57
|
+
let poller: Timer | undefined
|
|
58
|
+
|
|
59
|
+
/* handle the still unsubmitted prompt of the user: while the intent band,
|
|
60
|
+
the synopsis form, or the manpage browser is shown, every edit is a key of
|
|
61
|
+
it, else act on the typed trigger suffix at the end of the prompt only
|
|
62
|
+
(toggling the manual band for a skill command with "//", resp. showing the
|
|
63
|
+
synopsis form for a skill command with a manual page with "!!", else arming
|
|
64
|
+
the matching of the prompt as an intent with "!!", resp. showing the manpage
|
|
65
|
+
browser for any other prompt with "//"), close the manual band once the
|
|
66
|
+
prompt left its skill command or another band opened, track the skill
|
|
67
|
+
command of the hint band, and track the picks of the grill table inside
|
|
68
|
+
the edited prompt while no band is shown */
|
|
247
69
|
on("prompt.edit", async ($, e, next) => {
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
70
|
+
const band = shown()
|
|
71
|
+
const paged = manual.active()
|
|
72
|
+
const read = (file: string) => $.fs.read(`${$.plugin.root}/${file}`)
|
|
73
|
+
const dirs = async (dir: string) => (await $.fs.list(`${$.plugin.root}/${dir}`))
|
|
74
|
+
.filter((entry) => entry.kind === "dir" || entry.isLink).map((entry) => entry.name)
|
|
75
|
+
const open = async (box: PromptBox) =>
|
|
76
|
+
await manual.toggle(box, { read }) ?? await skill.open(box, { read }) ?? intent.open(box, {
|
|
77
|
+
read,
|
|
78
|
+
invalidate: () => $.ui.invalidate("ui.render"),
|
|
79
|
+
fork: (prompt) => $.model.fork({ prompt }),
|
|
80
|
+
complete: (request) => $.model.complete(request),
|
|
81
|
+
sleep: (ms) => $.clock.sleep(ms)
|
|
82
|
+
}) ?? await browse.open(box, { read, dirs }) ?? box
|
|
83
|
+
let box: PromptBox
|
|
84
|
+
if (band !== undefined) {
|
|
85
|
+
const result = band.edit(e)
|
|
86
|
+
box = typeof result !== "string" ? result :
|
|
87
|
+
await next({ ...e, text: result, cursor: result.length, start: result.length, end: result.length })
|
|
88
|
+
|
|
89
|
+
/* switch over from a closed band to another one by the trigger it left in the prompt */
|
|
90
|
+
if (shown() === undefined && TRIGGER.test(box.text))
|
|
91
|
+
box = await open(box)
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
box = await next(e)
|
|
95
|
+
if (/[!/]$/.test(e.inputText) && box.cursor === box.text.length && TRIGGER.test(box.text))
|
|
96
|
+
box = await open(box)
|
|
255
97
|
}
|
|
98
|
+
manual.track(box.text)
|
|
99
|
+
if (shown() !== undefined)
|
|
100
|
+
manual.reset()
|
|
101
|
+
const hinted = await hint.track(box.text, { read })
|
|
102
|
+
if (poller === undefined && partial.test(box.text)) {
|
|
103
|
+
const stop = () => {
|
|
104
|
+
timer.cancel()
|
|
105
|
+
poller = undefined
|
|
106
|
+
}
|
|
107
|
+
const timer = $.clock.every(100, () => {
|
|
108
|
+
$.prompt.read().then(async ({ text }) => {
|
|
109
|
+
if (!partial.test(text))
|
|
110
|
+
stop()
|
|
111
|
+
if (shown() === undefined && await hint.track(text, { read }))
|
|
112
|
+
$.ui.invalidate("ui.render")
|
|
113
|
+
}).catch(stop)
|
|
114
|
+
})
|
|
115
|
+
poller = timer
|
|
116
|
+
}
|
|
117
|
+
if (shown() !== undefined || grill.track(box.text) || band !== undefined || paged || manual.active() || hinted)
|
|
118
|
+
$.ui.invalidate("ui.render")
|
|
256
119
|
return box
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
frozen.set(latest, pick(e.text))
|
|
263
|
-
latest = undefined
|
|
264
|
-
picks = new Map()
|
|
265
|
-
selected = undefined
|
|
120
|
+
}).catch(($, e, next) => {
|
|
121
|
+
/* close a failed intent band resp. synopsis form and restore the prompt
|
|
122
|
+
resp. the command as typed (instead of the held prompt), else let the
|
|
123
|
+
edit pass */
|
|
124
|
+
const text = cancel()
|
|
266
125
|
$.ui.invalidate("ui.render")
|
|
267
|
-
return next(e)
|
|
126
|
+
return text !== undefined ? { text, cursor: text.length } : next(e)
|
|
268
127
|
})
|
|
269
128
|
|
|
270
|
-
/*
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
answers: row.answers.map((answer) =>
|
|
304
|
-
style(answer.text, chosen?.get(row.number) === answer.letter ? PICKED : undefined))
|
|
129
|
+
/* operate the synopsis form resp. the intent band, else scroll the manual
|
|
130
|
+
page in the band, by the wheel, and never let the engine move the window
|
|
131
|
+
over our (overflowing) band, even while it takes no scrolling */
|
|
132
|
+
on("ui.scroll", { component: "AbovePrompt" }, ($, e, next) => {
|
|
133
|
+
if (!bands.some((band) => band.scroll(e)) && !manual.scroll(e))
|
|
134
|
+
return shown() === undefined && !manual.active() ? next(e) : {}
|
|
135
|
+
$.ui.invalidate("ui.render")
|
|
136
|
+
return {}
|
|
137
|
+
}).catch(($, e, next) =>
|
|
138
|
+
/* let a failed scrolling of the synopsis form, the intent band, resp. the manual page pass */
|
|
139
|
+
next(e))
|
|
140
|
+
|
|
141
|
+
/* operate the intent band and the synopsis form by RETURN, resp. freeze the
|
|
142
|
+
picks in the latest table and close the intent band and the synopsis form
|
|
143
|
+
once the prompt is submitted */
|
|
144
|
+
on("prompt.submit", ($, e, next) => {
|
|
145
|
+
/* while the intent band or the synopsis form is shown, RETURN at the prompt
|
|
146
|
+
(which usually arrives as an edit of the held prompt instead, so this is
|
|
147
|
+
the fallback only) is a key of it, entering nothing and refilling the
|
|
148
|
+
prompt (immediately, and again whenever the submission cleared it or
|
|
149
|
+
restored the submitted prompt, as a duplicate of or next to the refilled
|
|
150
|
+
one, within a grace period) */
|
|
151
|
+
const band = shown()
|
|
152
|
+
if (band !== undefined && e.origin.kind === "composer") {
|
|
153
|
+
const text = band.submit()
|
|
154
|
+
const generation = ++submissions
|
|
155
|
+
$.ui.invalidate("ui.render")
|
|
156
|
+
const fill = async () => {
|
|
157
|
+
const box = await $.prompt.fill({ text })
|
|
158
|
+
if (shown() === undefined) {
|
|
159
|
+
grill.refresh(box.text)
|
|
160
|
+
$.ui.invalidate("ui.render")
|
|
161
|
+
}
|
|
305
162
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
for (let i = 0, j = 0; i < chars.length; i = j) {
|
|
320
|
-
const { bold, color } = chars[i]!
|
|
321
|
-
while (j < chars.length && chars[j]!.bold === bold && chars[j]!.color === color)
|
|
322
|
-
j++
|
|
323
|
-
runs.push({ text: chars.slice(i, j).map((char) => char.ch).join(""), bold, color })
|
|
163
|
+
const refill = async () => {
|
|
164
|
+
await fill()
|
|
165
|
+
const prompt = text.trim()
|
|
166
|
+
const submitted = e.text.trim()
|
|
167
|
+
for (const ms of [ 25, 50, 100, 200, 400 ]) {
|
|
168
|
+
await $.clock.sleep(ms)
|
|
169
|
+
const box = (await $.prompt.read()).text
|
|
170
|
+
if (generation !== submissions)
|
|
171
|
+
return
|
|
172
|
+
if (box === "" || box.indexOf(prompt) !== box.lastIndexOf(prompt)
|
|
173
|
+
|| (submitted !== prompt && box.includes(submitted)))
|
|
174
|
+
await fill()
|
|
175
|
+
}
|
|
324
176
|
}
|
|
325
|
-
|
|
177
|
+
refill().catch(() => {
|
|
178
|
+
/* ignore a prompt which is no longer accessible */
|
|
179
|
+
})
|
|
180
|
+
return { text: e.text }
|
|
326
181
|
}
|
|
327
182
|
|
|
328
|
-
/*
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
]
|
|
183
|
+
/* let any other submission (like a task notification) pass while the
|
|
184
|
+
intent band or the synopsis form is shown, as it leaves the held
|
|
185
|
+
prompt in place */
|
|
186
|
+
if (band !== undefined)
|
|
187
|
+
return next(e)
|
|
334
188
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
189
|
+
submissions++
|
|
190
|
+
grill.submit(e.text)
|
|
191
|
+
reset()
|
|
192
|
+
manual.reset()
|
|
193
|
+
hint.reset()
|
|
194
|
+
$.ui.invalidate("ui.render")
|
|
195
|
+
return next(e)
|
|
196
|
+
}).catch(($, e, next) =>
|
|
197
|
+
/* never let a failed RETURN of the intent band or synopsis form enter the prompt */
|
|
198
|
+
next.called || shown() === undefined ? next(e) :
|
|
199
|
+
{ drop: "intent band or synopsis form failed" })
|
|
200
|
+
|
|
201
|
+
/* redraw the replies which contain a grilling table (terminal only), where
|
|
202
|
+
the chain beneath runs, although its drawing is replaced by us (but this
|
|
203
|
+
at least silences a warning in the debug output of Claude Code) */
|
|
204
|
+
on("ui.render", { component: "AssistantMessage", surface: "terminal" }, async ($, e, next) => {
|
|
205
|
+
const drawn = await next(e)
|
|
206
|
+
return grill.render(e, $.ui.resolve(e), {
|
|
207
|
+
invalidate: () => $.ui.invalidate("ui.render"),
|
|
208
|
+
prompt: () => $.prompt.read(),
|
|
209
|
+
fill: (text) => $.prompt.fill({ text })
|
|
210
|
+
}) ?? drawn
|
|
211
|
+
})
|
|
349
212
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
hover: { scope: group, inverse: true, bold } }))
|
|
365
|
-
return [
|
|
366
|
-
Box({ key: `${group}/${line}`, hover: { scope: group }, children: [
|
|
367
|
-
Text({ children: paint(chars, w) }),
|
|
368
|
-
Box({ position: "absolute", top: 0, left: 0, display: "none",
|
|
369
|
-
hover: { scope: group, display: "flex" }, flexDirection: "row", children: buttons })
|
|
370
|
-
] }),
|
|
371
|
-
Text({ children: " ".repeat(Math.max(0, span - w)) })
|
|
372
|
-
]
|
|
213
|
+
/* draw the intent band resp. the synopsis form, else the manual page,
|
|
214
|
+
else the argument hint, into the band above the prompt (terminal only) */
|
|
215
|
+
on("ui.render", { component: "AbovePrompt", surface: "terminal" }, async ($, e, next) => {
|
|
216
|
+
const drawn = await next(e)
|
|
217
|
+
const ui = $.ui.resolve(e)
|
|
218
|
+
return shown()?.render(e, ui) ?? manual.render(e, ui) ?? hint.render(e, ui) ?? drawn
|
|
219
|
+
}).catch(($, e, next) => {
|
|
220
|
+
/* close a failed intent band resp. synopsis form, restore the prompt
|
|
221
|
+
resp. the command as typed (instead of the held prompt), resp. close
|
|
222
|
+
a failed manual band and hint band, and keep the drawing beneath */
|
|
223
|
+
const text = cancel()
|
|
224
|
+
if (text === undefined) {
|
|
225
|
+
manual.reset()
|
|
226
|
+
hint.reset()
|
|
373
227
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
Text({ wrap: "truncate-end", children: l + "─".repeat(w1 + 2) + m + "─".repeat(w2 + 2) + r })
|
|
378
|
-
const grid: RenderElement[] = [ rule("┌", "┬", "┐") ]
|
|
379
|
-
grid.push(Text({ wrap: "truncate-end", children:
|
|
380
|
-
[ "│ ", ...paint(head[0]!, w1), " │ ", ...paint(head[1]!, w2), " │" ] }))
|
|
381
|
-
grid.push(rule("├", "┼", "┤"))
|
|
382
|
-
rows.forEach((row, k) => {
|
|
383
|
-
const number = table.rows[k]!.number
|
|
384
|
-
const q = wrap(row.question, w1, row.indent, row.lead).map((line, i) =>
|
|
385
|
-
draw(line, w1, `q${number}`, i, press(number)))
|
|
386
|
-
const a = row.answers.flatMap((answer, j) => {
|
|
387
|
-
const letter = table.rows[k]!.answers[j]!.letter
|
|
388
|
-
return wrap(answer, w2, INDENT).map((line, i) =>
|
|
389
|
-
draw(line, w2, `a${number}${letter}`, i, press(number, letter)))
|
|
228
|
+
if (text !== undefined)
|
|
229
|
+
$.prompt.fill({ text }).catch(() => {
|
|
230
|
+
/* ignore a prompt which is no longer accessible */
|
|
390
231
|
})
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
Text({ children: "│ " }),
|
|
394
|
-
...(q[i] ?? [ Text({ children: " ".repeat(w1) }) ]),
|
|
395
|
-
Text({ children: " │ " }),
|
|
396
|
-
...(a[i] ?? [ Text({ children: " ".repeat(w2) }) ]),
|
|
397
|
-
Text({ children: " │" })
|
|
398
|
-
] }))
|
|
399
|
-
grid.push(k < rows.length - 1 ? rule("├", "┼", "┤") : rule("└", "┴", "┘"))
|
|
400
|
-
})
|
|
232
|
+
return next(e)
|
|
233
|
+
})
|
|
401
234
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
235
|
+
/* drop the one-line (and hence usually truncated) argument hint of the
|
|
236
|
+
skills from the typeahead and "/help", as the hint band shows it entirely */
|
|
237
|
+
on("command.describe", async ($, e, next) => {
|
|
238
|
+
const result = await next(e)
|
|
239
|
+
const command = /^(?:ase:)?(ase-[a-z0-9]+-[a-z0-9]+)$/.exec(e.command)
|
|
240
|
+
const read = (file: string) => $.fs.read(`${$.plugin.root}/${file}`)
|
|
241
|
+
if (command === null || await hint.synopsis(command[1]!, { read }) === undefined)
|
|
242
|
+
return result
|
|
243
|
+
return { description: result.description, isHidden: result.isHidden }
|
|
411
244
|
})
|
|
412
245
|
}
|
|
413
246
|
|