@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.
@@ -0,0 +1,142 @@
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
+ Manual band, toggling for a skill command (bare or with its options and
9
+ arguments already typed), once suffixed with a blank and "//", the entire
10
+ manual page of the skill in the band above the prompt (scrolled by the
11
+ wheel), while the "//" is removed again from the prompt, so the command
12
+ can be typed on, and the band also closes once the prompt no longer starts
13
+ with the skill command, the synopsis form opens, or the prompt is
14
+ submitted. */
15
+
16
+ import type { Elements, PromptBox, RenderElement } from "claude-code"
17
+ import { type Engine, type Char, type BandRender, type BandScroll, type View,
18
+ uniform, flow, layout, wheel, panel } from "./ase-mods-common.ts"
19
+
20
+ /* the trigger of a manual band inside the prompt (a skill command suffixed
21
+ with a blank and "//", with its typed options and arguments, if any,
22
+ separated by a blank from it), the skill command at the start of the prompt,
23
+ the share of the terminal height taken by the band outside fullscreen, the
24
+ indentation of the section bodies, and the title placed into the top edge
25
+ of the band */
26
+ const MANUAL = /^\/(?:ase:)?(ase-[a-z0-9]+-[a-z0-9]+)(?:\s[^]*)?\s\/\/$/
27
+ const COMMAND = /^\/(?:ase:)?(ase-[a-z0-9]+-[a-z0-9]+)(?![\w-])/
28
+ const HEIGHT = 0.5
29
+ const INDENT = 2
30
+ const TITLE = "[ ⧉ ASE: Show Skill Manpage ]"
31
+
32
+ /* a manual band: the scroll state of its frame, the skill, its manual page,
33
+ and the manual page lines with the columns they were wrapped to */
34
+ type Manual = View & {
35
+ skill: string,
36
+ manual: string,
37
+ lines: Char[][],
38
+ columns: number
39
+ }
40
+
41
+ /* convert a manual page into lines of styled characters, hard-wrapped to the
42
+ given columns: each section with its heading in bold and its body indented
43
+ (with the lines of the SYNOPSIS kept apart, as in the manual page) */
44
+ export const page = (manual: string, columns: number): Char[][] => {
45
+ const lines: Char[][] = []
46
+ for (const part of manual.split(/^(?=## {2})/m).filter((part) => part.startsWith("## "))) {
47
+ const [ head, ...body ] = part.split("\n")
48
+ const name = head!.replace(/^## {2}/, "").trim()
49
+ const text = name === "SYNOPSIS" ?
50
+ body.filter((line) => line.trim() !== "").flatMap((line) => flow(line, columns - INDENT, true)) :
51
+ flow(body.join("\n"), columns - INDENT, true)
52
+ if (lines.length > 0)
53
+ lines.push([])
54
+ lines.push(uniform(name, true, undefined),
55
+ ...text.map((line) => line.length > 0 ? [ ...uniform(" ".repeat(INDENT), false, undefined), ...line ] : line))
56
+ }
57
+ return lines
58
+ }
59
+
60
+ /* the manual band: its state and its operations */
61
+ export const modHelpShow = () => {
62
+ /* the manual pages of the skills (loaded on first use, undefined
63
+ for a skill without a manual page) and the currently shown band */
64
+ const manuals = new Map<string, string | undefined>()
65
+ let shown: Manual | undefined
66
+
67
+ /* determine whether the manual band is shown */
68
+ const active = () => shown !== undefined
69
+
70
+ /* close the manual band */
71
+ const reset = () => {
72
+ shown = undefined
73
+ }
74
+
75
+ /* determine the manual page lines of the manual band, wrapped to the given columns */
76
+ const wrapped = (current: Manual, columns: number): Char[][] => {
77
+ if (current.columns !== columns) {
78
+ current.lines = page(current.manual, columns)
79
+ current.columns = columns
80
+ }
81
+ return current.lines
82
+ }
83
+
84
+ /* show the manual band of a skill with a manual page (loaded on first use),
85
+ and tell whether it is shown */
86
+ const show = async (skill: string, engine: Pick<Engine, "read">): Promise<boolean> => {
87
+ if (!manuals.has(skill))
88
+ manuals.set(skill, await engine.read(`skills/${skill}/help.md`).catch(() => undefined))
89
+ const manual = manuals.get(skill)
90
+ if (manual === undefined)
91
+ return false
92
+ shown = { skill, manual, lines: [], columns: 0, top: 0, scroll: 0, page: 1, split: 0 }
93
+ return true
94
+ }
95
+
96
+ /* for a skill command with a manual page, once suffixed with the trigger,
97
+ toggle its manual band (closing it, if already shown for the skill, else
98
+ showing it) and remove the trigger from the prompt again */
99
+ const toggle = async (box: PromptBox, engine: Pick<Engine, "read">): Promise<PromptBox | undefined> => {
100
+ const command = MANUAL.exec(box.text)
101
+ if (command === null)
102
+ return undefined
103
+ if (shown?.skill === command[1])
104
+ shown = undefined
105
+ else if (!await show(command[1]!, engine))
106
+ return undefined
107
+ const text = box.text.slice(0, -2)
108
+ return { text, cursor: text.length }
109
+ }
110
+
111
+ /* track the prompt and close the manual band once the prompt no longer
112
+ starts with its skill command */
113
+ const track = (text: string): void => {
114
+ if (shown !== undefined && COMMAND.exec(text)?.[1] !== shown.skill)
115
+ shown = undefined
116
+ }
117
+
118
+ /* scroll the manual page in the band by the wheel
119
+ (and tell whether the band took the scrolling) */
120
+ const scroll = (e: BandScroll): boolean => {
121
+ if (shown === undefined)
122
+ return false
123
+ wheel(e, shown)
124
+ return true
125
+ }
126
+
127
+ /* draw the manual page into the band above the prompt (terminal only),
128
+ yielding to a survey */
129
+ const render = (e: BandRender, ui: Elements["terminal"]): RenderElement | undefined => {
130
+ if (shown === undefined || e.props.hasSurvey)
131
+ return undefined
132
+ const geometry = layout(e, 0, HEIGHT)
133
+ return panel(ui, geometry, shown, {
134
+ title: TITLE,
135
+ hints: (geometry.overflow ? "wheel or CTRL+x + TAB + ↑/↓/PgUp/PgDn: scroll · " : "") +
136
+ "//: close · !!: Compose Skill Command · (keep typing the command)",
137
+ lines: wrapped(shown, geometry.span - 3)
138
+ })
139
+ }
140
+
141
+ return { active, reset, toggle, track, scroll, render }
142
+ }