@rse/ase 2.0.6 → 2.0.8

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 (68) hide show
  1. package/dst/ase-agent-definition-api.d.ts +2 -0
  2. package/dst/ase-agent-definition-core.js +24 -3
  3. package/dst/ase-agent-fleet.js +2 -1
  4. package/dst/ase-agent-relay-client.js +3 -4
  5. package/dst/ase-agent-runtime-engine.js +22 -4
  6. package/dst/ase-agent-runtime-tool.js +10 -8
  7. package/dst/ase-agent-runtime.js +8 -4
  8. package/dst/ase-config-schema.js +4 -1
  9. package/dst/ase-lib-fetch.js +12 -0
  10. package/dst/ase-lib-server-cli.js +8 -5
  11. package/dst/ase-man-catalog.js +1 -1
  12. package/dst/ase-task-board-diff.js +120 -0
  13. package/dst/ase-task-board-tui-control.js +8 -5
  14. package/dst/ase-task-board-tui-model.js +54 -14
  15. package/dst/ase-task-board-tui-popup.js +41 -8
  16. package/dst/ase-task-board-tui.js +42 -27
  17. package/dst/ase-task-board-web-client.css +1 -1
  18. package/dst/ase-task-board-web-client.html +16 -0
  19. package/dst/ase-task-board-web-client.js +33 -33
  20. package/dst/ase-task-board-web-server.js +44 -9
  21. package/dst/ase-task-board.js +12 -4
  22. package/dst/ase-task-store-client.js +4 -5
  23. package/dst/ase-task-store-plugin-gitea.js +3 -3
  24. package/dst/man/ase-agent-adhoc.md +25 -6
  25. package/dst/man/ase-agent-condenser.md +7 -0
  26. package/dst/man/ase-agent-editor.md +7 -0
  27. package/dst/man/ase-agent-estimator.md +7 -0
  28. package/dst/man/ase-agent-fleet.md +7 -4
  29. package/dst/man/ase-agent-implementer.md +7 -0
  30. package/dst/man/ase-agent-integrator.md +7 -0
  31. package/dst/man/ase-agent-qualifier.md +7 -0
  32. package/dst/man/ase-agent-rebooter.md +7 -0
  33. package/dst/man/ase-agent-researcher.md +7 -0
  34. package/dst/man/ase-config-init.md +2 -1
  35. package/dst/man/ase-config.md +5 -0
  36. package/dst/man/ase-task-board.md +44 -2
  37. package/package.json +11 -10
  38. package/plugin/.claude-plugin/plugin.json +1 -1
  39. package/plugin/.codex-plugin/plugin.json +1 -1
  40. package/plugin/.github/plugin/plugin.json +1 -1
  41. package/plugin/meta/ase-common-task.md +33 -1
  42. package/plugin/meta/ase-control.md +14 -0
  43. package/plugin/meta/ase-format-specbook.yaml +9 -9
  44. package/plugin/meta/ase-skill.md +7 -0
  45. package/plugin/mods/ase-mods-common.ts +73 -42
  46. package/plugin/mods/ase-mods-help-hint.ts +1 -1
  47. package/plugin/mods/ase-mods-task-edit.ts +427 -0
  48. package/plugin/mods/ase-mods.ts +45 -14
  49. package/plugin/skills/ase-code-edit/SKILL.md +53 -16
  50. package/plugin/skills/ase-code-edit/help.md +26 -13
  51. package/plugin/skills/ase-code-lint/SKILL.md +7 -3
  52. package/plugin/skills/ase-help-intent/data.md +248 -36
  53. package/plugin/skills/ase-help-skill/catalog.md +1 -0
  54. package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
  55. package/plugin/skills/ase-repo-changelog/SKILL.md +29 -10
  56. package/plugin/skills/ase-repo-changelog/help.md +5 -1
  57. package/plugin/skills/ase-repo-commit/SKILL.md +4 -1
  58. package/plugin/skills/ase-repo-dissect/help.md +2 -1
  59. package/plugin/skills/ase-repo-stage/SKILL.md +459 -0
  60. package/plugin/skills/ase-repo-stage/help.md +120 -0
  61. package/plugin/skills/ase-spec-edit/SKILL.md +20 -12
  62. package/plugin/skills/ase-spec-edit/help.md +22 -14
  63. package/plugin/skills/ase-task-edit/SKILL.md +114 -9
  64. package/plugin/skills/ase-task-edit/help.md +60 -0
  65. package/plugin/skills/ase-task-grill/SKILL.md +35 -20
  66. package/plugin/skills/ase-task-grill/help.md +18 -8
  67. /package/plugin/mods/{ase-mods-evaluate.ts → ase-mods-meta-evaluate.ts} +0 -0
  68. /package/plugin/mods/{ase-mods-grill.ts → ase-mods-task-grill.ts} +0 -0
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "https://ase.tools",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "https://github.com/rse/ase/issues" },
9
- "version": "2.0.6",
9
+ "version": "2.0.8",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -31,7 +31,7 @@
31
31
  "globals": "17.14.0",
32
32
  "typescript": "6.0.3",
33
33
  "eslint-plugin-vue": "10.12.0",
34
- "vue-eslint-parser": "10.4.1",
34
+ "vue-eslint-parser": "10.4.2",
35
35
  "vue-tsc": "3.3.14",
36
36
 
37
37
  "@rse/stx": "1.1.7",
@@ -39,9 +39,9 @@
39
39
  "shx": "0.4.0",
40
40
  "sync-directory": "6.0.6",
41
41
  "stylus": "0.64.0",
42
- "vite": "8.3.5",
42
+ "vite": "8.3.6",
43
43
  "@vitejs/plugin-vue": "6.0.10",
44
- "vue": "3.5.44",
44
+ "vue": "3.5.45",
45
45
  "mousetrap": "1.6.5",
46
46
  "@codemirror/view": "6.43.15",
47
47
  "@codemirror/state": "6.7.6",
@@ -78,7 +78,7 @@
78
78
  "valibot": "1.5.0",
79
79
  "execa": "10.2.1",
80
80
  "mkdirp": "3.0.1",
81
- "@hapi/hapi": "21.4.12",
81
+ "@hapi/hapi": "21.4.13",
82
82
  "beautiful-mermaid": "1.1.3",
83
83
  "@d2lang/d2": "0.1.34",
84
84
  "elkjs": "0.12.1",
@@ -86,8 +86,9 @@
86
86
  "string-width": "8.3.0",
87
87
  "react": "19.3.1",
88
88
  "chokidar": "6.0.0",
89
- "marked": "18.1.2",
90
- "shiki": "4.5.0",
89
+ "marked": "18.1.3",
90
+ "diff": "9.0.0",
91
+ "shiki": "4.6.0",
91
92
  "cli-table3": "0.6.5",
92
93
  "chalk": "6.0.1",
93
94
  "pretty-ms": "10.0.0",
@@ -97,7 +98,7 @@
97
98
  "zod": "4.6.7",
98
99
  "which": "7.0.0",
99
100
  "update-notifier": "7.3.1",
100
- "semver": "7.8.5",
101
+ "semver": "7.8.6",
101
102
  "shell-quote": "1.12.0",
102
103
  "get-stdin": "10.0.0",
103
104
  "proper-lockfile": "4.1.2",
@@ -105,10 +106,10 @@
105
106
  "p-queue": "10.0.0",
106
107
  "pacote": "22.0.1",
107
108
  "ofetch": "1.5.1",
108
- "undici": "8.12.3",
109
+ "undici": "8.12.4",
109
110
  "picomatch": "4.0.7",
110
111
  "lru-cache": "11.5.3",
111
- "@rse/specbook": "1.6.1",
112
+ "@rse/specbook": "1.6.2",
112
113
  "markdownlint": "0.42.0",
113
114
  "tail": "2.2.6",
114
115
  "source-code-error": "1.2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -58,6 +58,30 @@ Task Skill Common Steps
58
58
 
59
59
  </define>
60
60
 
61
+ <define name="task-focus">
62
+
63
+ Resolve the plan sections under focus: treat <getopt-option-focus/> as a
64
+ comma-separated list of *section tokens*. Trim every token, then expand
65
+ the synonyms `SPEC` and `WHAT` to `SPECIFICATION`, the synonyms `DES` and
66
+ `HOW` to `DESIGN`, and the synonyms `VERIFY`, `VER`, and `WHEN` to
67
+ `VERIFICATION`, and expand the sentinel `all` (the default) to
68
+ `SPECIFICATION,DESIGN,VERIFICATION`. Set <sections/> to the resulting
69
+ list, *keeping the given order*. The getopt parser validates only the
70
+ *first* token, so you *MUST* validate each token yourself: if any token
71
+ is *empty*, *not* one of the recognized sections or synonyms (matched
72
+ *case-sensitively*), or results in a section already present in
73
+ <sections/>, bind <token/> to that offending token, then only output the
74
+ following <template/> and immediately *STOP* processing the entire
75
+ current skill:
76
+
77
+ <template>
78
+ ⧉ **ASE**: ☻ skill: **<arg1/>**, ▶ ERROR: invalid `--focus` token: **<token/>**
79
+ </template>
80
+
81
+ Do not output anything else.
82
+
83
+ </define>
84
+
61
85
  <define name="task-load-content">
62
86
 
63
87
  <if condition="
@@ -227,10 +251,18 @@ related to this MCP call except the following <template/>:
227
251
  custom dialog according to the expanded `custom-dialog`
228
252
  definition. You *MUST* closely follow this definition:
229
253
 
230
- <expand name="custom-dialog" arg1="--no-other">
254
+ <expand name="custom-dialog" arg1="--other">
231
255
  <content/>
232
256
  </expand>
233
257
 
258
+ If <result/> matches `OTHER: <text/>`, the user typed a free-text
259
+ request instead of choosing an option: treat it as an implicit
260
+ `DONE`, i.e., set <result>DONE</result>, and let the calling skill
261
+ finish along its `DONE` dispatch (output its `DONE` template).
262
+ Its *STOP* then ends only the skill, *not* the current turn:
263
+ immediately afterwards, handle <text/> as a regular user request,
264
+ outside of any skill, without re-opening this dialog.
265
+
234
266
  </define>
235
267
 
236
268
  <define name="task-next-handoff">
@@ -187,3 +187,17 @@ Control Flow Constructs
187
187
  the order of their occurrence in <parallel-body/>. This construct is
188
188
  expanded to its <parallel-body/>. Do not output anything else.
189
189
 
190
+ - *IMPORTANT*: You *MUST* honor the following rule for *all*
191
+ concurrently dispatched sub-agents -- whether invoked through
192
+ <parallel/> or through multiple `Agent` tool invocations emitted in
193
+ one single message:
194
+
195
+ The agent tool may run sub-agents in the *background* (e.g.
196
+ *Anthropic Claude Code CLI* ignores `run_in_background: false`) and
197
+ then notifies you of each returned sub-agent individually. While
198
+ these notifications arrive, you *MUST* *NOT* output anything -- no
199
+ progress messages (like `Batch 3 of 8 is back, 5 still running`),
200
+ no intermediate results, and no waiting notices -- but silently wait
201
+ for the remaining sub-agents. Only once *all* of them have returned,
202
+ continue with the processing which consumes their results.
203
+
@@ -125,7 +125,7 @@
125
125
  id: DG
126
126
  file: 03-REQS-DG-Domain-Glossary.md
127
127
  desc: >
128
- The ubiquitous language of the domain, defining the meaning of each domain term in business language, together with its synonyms, its place in the hierarchy of terms, and the terms it has to be kept apart from, shared consistently across all Artifacts.
128
+ The ubiquitous language of the domain, defining the meaning of each domain term in business language, together with its synonyms, its place in the hierarchy of terms, and the terms it has to be kept apart from, shared consistently across all artifacts.
129
129
  optional: true
130
130
  children:
131
131
  - kind: TERM
@@ -581,7 +581,7 @@
581
581
  optional: true
582
582
  - name: TRIGGER
583
583
  desc: >
584
- The event that starts the use case, stated as a 1-sentence occurrence (e.g. `The attendee opens the event URL.`), as opposed to the `PRE-CONDITION` which is a state that has to hold, and matching the `When <trigger>` of the event-driven Functional Requirements the use case exercises.
584
+ The event that starts the use case, stated as a 1-sentence occurrence (e.g. `The attendee opens the event URL.`), as opposed to the `PRE-CONDITION`, which is a state that has to hold, and matching the `When <trigger>` of the event-driven Functional Requirements the use case exercises.
585
585
  optional: true
586
586
  - name: POST-CONDITION
587
587
  desc: >
@@ -847,7 +847,7 @@
847
847
  The kind of the interface, one of:
848
848
  - `REST`: HTTP resources addressed by URL paths and HTTP methods.
849
849
  - `GraphQL`: A GraphQL schema of queries, mutations, and subscriptions.
850
- - `gRPC`: Protocol Buffers based remote procedure calls.
850
+ - `gRPC`: Protocol Buffers-based remote procedure calls.
851
851
  - `WebSocket`: Messages exchanged over a WebSocket connection.
852
852
  - `MQTT`: Messages published to and subscribed from MQTT topics.
853
853
  - `MCP`: Tools, resources, and prompts of a Model Context Protocol server.
@@ -1322,7 +1322,7 @@
1322
1322
  props:
1323
1323
  - name: MAXIMS
1324
1324
  desc: >
1325
- Comma-separated references to the MAXIM objects the principle concretizes (e.g. a `Stateless Services` principle concretizing a `Scale Out over Scale Up` maxim), so that the principles form a hierarchy below the maxims rather than a flat catalogue; omitted for a principle standing on its own forces alone.
1325
+ Comma-separated references to the MAXIM objects the principle concretizes (e.g. a `Stateless Services` principle concretizing a `Scale Out over Scale Up` maxim), so that the principles form a hierarchy below the maxims rather than a flat catalog; omitted for a principle standing on its own forces alone.
1326
1326
  value: list([[MAXIM:*]])
1327
1327
  optional: true
1328
1328
  - name: DRIVEN-BY
@@ -1362,7 +1362,7 @@
1362
1362
  optional: true
1363
1363
  - name: USES
1364
1364
  desc: >
1365
- Comma-separated references to the smaller ARCHITECTURE-PATTERNS objects the pattern is composed of (e.g. a `Saga` using a `Transactional Outbox` for its steps), so that the patterns form a language of related structures rather than a flat catalogue.
1365
+ Comma-separated references to the smaller ARCHITECTURE-PATTERNS objects the pattern is composed of (e.g. a `Saga` using a `Transactional Outbox` for its steps), so that the patterns form a language of related structures rather than a flat catalog.
1366
1366
  value: list([[PATTERN:*]])
1367
1367
  optional: true
1368
1368
  acyclic: true
@@ -1663,7 +1663,7 @@
1663
1663
  desc: >
1664
1664
  A single technology component, named with a short (2-5 word) summary (e.g. `UI Framework`, `Web Server`, `Relational Database`) and described in a concise (1-3 sentence) paragraph by how the product is used within the solution and how it fits into the overall technology stack, plus a 1-sentence rationale ("why") for choosing this product over the alternatives.
1665
1665
 
1666
- A component states one product in one role on one tier: the product used by several tiers alike belongs to the `Common` tier once, the lock-in carrying trade-off behind the choice is a Design Decisions decision referencing the component through `DECIDES`, and the functional components depending on the product reference it through `USED-BY`.
1666
+ A component states one product in one role on one tier: the product used by several tiers alike belongs to the `Common` tier once, the lock-in-carrying trade-off behind the choice is a Design Decisions decision referencing the component through `DECIDES`, and the functional components depending on the product reference it through `USED-BY`.
1667
1667
  props:
1668
1668
  - name: PRODUCT
1669
1669
  desc: >
@@ -2395,7 +2395,7 @@
2395
2395
  optional: true
2396
2396
  - name: USES
2397
2397
  desc: >
2398
- Comma-separated references to the smaller CODE-PATTERNS objects the pattern is composed of (e.g. an `Endpoint Authorization` using the `Error Propagation` for its rejections), so that the patterns form a language of related shapes rather than a flat catalogue.
2398
+ Comma-separated references to the smaller CODE-PATTERNS objects the pattern is composed of (e.g. an `Endpoint Authorization` using the `Error Propagation` for its rejections), so that the patterns form a language of related shapes rather than a flat catalog.
2399
2399
  value: list([[CODE-PATTERN:*]])
2400
2400
  optional: true
2401
2401
  acyclic: true
@@ -2449,8 +2449,8 @@
2449
2449
  The mandatory, concrete input data or action applied during the test, i.e. the stimulus whose response the `EXPECTED` judges.
2450
2450
  - name: EXPECTED
2451
2451
  desc: >
2452
- The mandatory, concrete, observable response of the solution to the `INPUT` (a message shown, a session issued, a submission rejected) that defines a passing test, as opposed to the `POST-CONDITION` which is the state left behind. When the test case verifies a NON-FUNCTIONAL-REQUIREMENTS object, the expected response restates the `METRIC` threshold of that requirement as the pass criterion (e.g. `p95 latency < 200 ms`) rather than a qualitative phrase (e.g. `remains responsive`).
2452
+ The mandatory, concrete, observable response of the solution to the `INPUT` (a message shown, a session issued, a submission rejected) that defines a passing test, as opposed to the `POST-CONDITION`, which is the state left behind. When the test case verifies a NON-FUNCTIONAL-REQUIREMENTS object, the expected response restates the `METRIC` threshold of that requirement as the pass criterion (e.g. `p95 latency < 200 ms`) rather than a qualitative phrase (e.g. `remains responsive`).
2453
2453
  - name: POST-CONDITION
2454
2454
  desc: >
2455
- The persisted state that must hold after the test completes (e.g. `the authorization token is in state used`), including the state that must not have changed (e.g. `no session token exists`), as opposed to the `EXPECTED` which is the immediate response, and omitted for a test whose input leaves no state behind.
2455
+ The persisted state that must hold after the test completes (e.g. `the authorization token is in state used`), including the state that must not have changed (e.g. `no session token exists`), as opposed to the `EXPECTED`, which is the immediate response, and omitted for a test whose input leaves no state behind.
2456
2456
  optional: true
@@ -232,6 +232,13 @@ Skill Identification
232
232
  ⧉ **ASE**: ✪ skill: **<skill-name/>**, ✦ purpose: **<purpose/>**, ▶ status: **skill started**
233
233
  </template>
234
234
 
235
+ You *MUST* output this <template/> *immediately* as the *very first*
236
+ output of the skill, *before* any tool call (including the
237
+ `ase_getopt` call) and *before* any work of the skill, and also when
238
+ the skill was invoked via the `Skill` tool from another skill. Any
239
+ "Do not output anything" instruction of the subsequent steps does
240
+ *NOT* suppress or defer it.
241
+
235
242
  Later (but only if ((`$1` is *NOT* equal to `-h` or `--help`) *AND*
236
243
  (<ase-guidance-level/> is equal to `normal` or `verbose`))), once
237
244
  this skill finally will stop processing, you *MUST* once output the
@@ -6,20 +6,21 @@
6
6
 
7
7
  /* Claude Code function hooks module ("mod", EARLY ACCESS):
8
8
  Common parts, shared by the grill table, the decision matrix, the intent
9
- help, the synopsis form, the manual band, the manpage browser, and the hint
10
- band: the rows of the Markdown tables, the styled characters of the terminal
9
+ help, the synopsis form, the manual band, the manpage browser, the hint band,
10
+ and the task band: the rows of the Markdown tables, the styled characters of the terminal
11
11
  drawing, their Markdown conversion, wrapping, and framing, and the contract,
12
12
  keys, geometry, and drawing of the master/detail view in the band above the
13
13
  prompt. */
14
14
 
15
15
  import type { Elements, Frozen, MatchedEvent, ModelCompleteRequest, ModelForkResult,
16
- PromptBox, PromptEditInput, PromptFilled, RenderElement } from "claude-code"
16
+ PromptBox, PromptEditInput, PromptFilled, RenderElement, Timer } from "claude-code"
17
17
 
18
18
  /* the services of the engine, bound by the hooking infrastructure for the
19
19
  modules (as the engine refuses to load a module passing "$" itself): the
20
20
  redrawing, the reading of a plugin file and of the names of the directories
21
21
  in a plugin directory, the forked and the plain model completion, the
22
- waiting, and the reading and filling of the prompt */
22
+ waiting, the reading and filling of the prompt, the loading of a task
23
+ (with its entity tag), and the running of a repeating timer */
23
24
  export type Engine = {
24
25
  invalidate: () => void,
25
26
  read: (file: string) => Promise<string>,
@@ -28,7 +29,9 @@ export type Engine = {
28
29
  complete: (request: ModelCompleteRequest) => Promise<string>,
29
30
  sleep: (ms: number) => Promise<void>,
30
31
  prompt: () => Promise<PromptBox>,
31
- fill: (text: string) => Promise<PromptFilled>
32
+ fill: (text: string) => Promise<PromptFilled>,
33
+ load: (id: string) => Promise<string>,
34
+ every: (ms: number, fn: () => void) => Timer
32
35
  }
33
36
 
34
37
  /* the colors of a picked (resp. selected or focused) item, of a code span,
@@ -131,7 +134,7 @@ export const cells = (line: string): string[] => {
131
134
  }
132
135
 
133
136
  /* a single styled character of the terminal drawing */
134
- export type Char = { ch: string, bold: boolean, color: string | undefined }
137
+ export type Char = { ch: string, bold: boolean, color: string | undefined, italic?: boolean, inverse?: boolean, background?: string }
135
138
 
136
139
  /* extract a section of a manual page (without its heading) */
137
140
  export const section = (manual: string, name: string): string =>
@@ -225,16 +228,21 @@ export const wrap = (chars: Char[], columns: number, indent: number, lead = 0):
225
228
  }
226
229
 
227
230
  /* a run of equally styled characters */
228
- type Run = { text: string, bold: boolean, color: string | undefined }
231
+ type Run = { text: string, bold: boolean, color: string | undefined, italic: boolean, inverse: boolean, background: string | undefined }
229
232
 
230
233
  /* split a line of styled characters into runs of equally styled characters */
231
234
  export const split = (chars: Char[]): Run[] => {
232
235
  const runs: Run[] = []
233
236
  for (let i = 0, j = 0; i < chars.length; i = j) {
234
237
  const { bold, color } = chars[i]!
235
- while (j < chars.length && chars[j]!.bold === bold && chars[j]!.color === color)
238
+ const italic = chars[i]!.italic ?? false
239
+ const inverse = chars[i]!.inverse ?? false
240
+ const { background } = chars[i]!
241
+ while (j < chars.length && chars[j]!.bold === bold && chars[j]!.color === color &&
242
+ (chars[j]!.italic ?? false) === italic && (chars[j]!.inverse ?? false) === inverse &&
243
+ chars[j]!.background === background)
236
244
  j++
237
- runs.push({ text: chars.slice(i, j).map((char) => char.ch).join(""), bold, color })
245
+ runs.push({ text: chars.slice(i, j).map((char) => char.ch).join(""), bold, color, italic, inverse, background })
238
246
  }
239
247
  return runs
240
248
  }
@@ -307,15 +315,15 @@ export const frame = (lines: Char[][], columns: number, color: string | undefine
307
315
  }
308
316
 
309
317
  /* split a horizontal edge of a frame into runs with a title placed after its
310
- first and another one before its last two characters (each if it fits, the
311
- one at the end with precedence), flagged whether they are a title */
312
- const titled = (line: string, head: string, tail: string): [ string, boolean ][] => {
313
- const end = tail !== "" && line.length >= tail.length + 4 ? tail : ""
314
- const start = head !== "" && line.length >= head.length + (end !== "" ? end.length + 6 : 4) ? head : ""
315
- const k = end !== "" ? line.length - 2 - end.length : line.length
318
+ first "lead" and another one before its last "trail" characters (each if it
319
+ fits, the one at the end with precedence), flagged whether they are a title */
320
+ const titled = (line: string, head: string, tail: string, lead: number, trail: number): [ string, boolean ][] => {
321
+ const end = tail !== "" && line.length >= tail.length + trail + 2 ? tail : ""
322
+ const start = head !== "" && line.length >= head.length + lead + trail + (end !== "" ? end.length + 2 : 0) ? head : ""
323
+ const k = end !== "" ? line.length - trail - end.length : line.length
316
324
  return [
317
- ...(start !== "" ? [ [ line.slice(0, 2), false ], [ start, true ] ] as [ string, boolean ][] : []),
318
- [ line.slice(start !== "" ? 2 + start.length : 0, k), false ],
325
+ ...(start !== "" ? [ [ line.slice(0, lead), false ], [ start, true ] ] as [ string, boolean ][] : []),
326
+ [ line.slice(start !== "" ? lead + start.length : 0, k), false ],
319
327
  ...(end !== "" ? [ [ end, true ], [ line.slice(k + end.length), false ] ] as [ string, boolean ][] : [])
320
328
  ]
321
329
  }
@@ -348,14 +356,15 @@ export type Text = Elements["terminal"]["Text"]
348
356
 
349
357
  /* draw a line of styled characters: its styled runs */
350
358
  export const paint = (Text: Text, chars: Char[]): (string | RenderElement)[] =>
351
- split(chars).map(({ text, bold, color }) =>
352
- bold || color !== undefined ? Text({ bold, color, children: text }) : text)
359
+ split(chars).map(({ text, bold, color, italic, inverse, background }) =>
360
+ bold || italic || inverse || color !== undefined || background !== undefined ?
361
+ Text({ bold, italic, inverse, color, backgroundColor: background, children: text }) : text)
353
362
 
354
363
  /* draw a horizontal edge of the self-drawn frame (dimmed), with titles at its
355
364
  start and its end (with their brackets dimmed, an "ASE" prefix before a colon
356
365
  in bold, and the rest dimmed) */
357
- const edge = (Text: Text, line: string, head = "", tail = ""): RenderElement =>
358
- Text({ children: titled(line, head, tail).flatMap(([ text, label ]) => {
366
+ const edge = (Text: Text, line: string, head = "", tail = "", lead = 2, trail = 2): RenderElement =>
367
+ Text({ children: titled(line, head, tail, lead, trail).flatMap(([ text, label ]) => {
359
368
  const m = /^(\[ )?([^:]*)(:.*?)( \])?$/.exec(text)
360
369
  if (!label || m === null)
361
370
  return [ Text({ ...BORDER, children: text }) ]
@@ -367,9 +376,10 @@ const edge = (Text: Text, line: string, head = "", tail = ""): RenderElement =>
367
376
  ]
368
377
  }) })
369
378
 
370
- /* draw a row of a scrollbar: the thumb, the (dimmed) track, or nothing */
379
+ /* draw a row of a scrollbar: the dark grey thumb (an inverse space, as it fills
380
+ the entire cell, unlike a block glyph), the (dimmed) track, or nothing */
371
381
  const bar = (Text: Text, mark: Mark | undefined): RenderElement =>
372
- mark === "thumb" ? Text({ children: "█" }) :
382
+ mark === "thumb" ? Text({ inverse: true, color: "ansi256(242)", children: " " }) :
373
383
  mark === "track" ? Text({ dimColor: true, children: "░" }) : Text({ children: " " })
374
384
 
375
385
  /* the geometry of the band above the prompt: the inner columns of the self-drawn
@@ -384,7 +394,7 @@ export type Layout = { span: number, body: number, master: number, detail: numbe
384
394
  and, once focused, by the keys only for a tree taller than the band), all rows of
385
395
  the band but the one of the engine's "n more" row (as the band then takes them
386
396
  anyway), and the columns of the master view (by the given share) and of the
387
- detail view (with a separator column between them, and a gap of two columns and
397
+ detail view (with a separator column between them, and a gap, a separator, and
388
398
  a scrollbar column right of the detail view) */
389
399
  export const layout = (e: BandRender, share: number, band = BAND): Layout => {
390
400
  const span = Math.max(20, e.props.bodyColumns - FRAME.cols)
@@ -412,10 +422,14 @@ export const wheel = (e: BandScroll, view: View, step?: (by: number) => void): v
412
422
 
413
423
  /* draw the band above the prompt: inside the self-drawn frame (with the title
414
424
  in its top edge) either a single view of the given rows, a single view of the
415
- given lines with its scrollbar, or a master view of the given rows (where the
416
- given rule rows are horizontal lines joining the borders) and, joined by a
417
- separator, a detail view of the given lines (optionally dimmed) with its
418
- scrollbar, into the bottom edge the given status (if any) at its start and
425
+ given lines with its scrollbar (optionally with the first given characters of
426
+ each line in a gutter at the left border, separated by a vertical line joining
427
+ the edges, with the background of a line filling the columns up to its scrollbar),
428
+ or a master view of the given rows (where the given rule rows are horizontal lines
429
+ joining the borders) and, joined by a separator, a detail view of the given lines
430
+ (optionally dimmed) with its scrollbar, every scrollbar (of lines not always
431
+ fitting) separated by a vertical line joining the edges,
432
+ into the bottom edge the given status (if any) at its start and
419
433
  the scroll position of the lines (unless they always fit) at its end, and below
420
434
  the frame the key hints (followed by blank rows for an overflowing band),
421
435
  where the scroll position is clamped, and the page size (for the scroll keys)
@@ -428,7 +442,8 @@ export const panel = (ui: Elements["terminal"], { span, body, master, detail, ov
428
442
  rule?: (i: number) => boolean,
429
443
  lines?: Char[][],
430
444
  dim?: boolean,
431
- fit?: boolean
445
+ fit?: boolean,
446
+ gutter?: number
432
447
  }): RenderElement => {
433
448
  const { Box, Text } = ui
434
449
  const lines = spec.lines ?? []
@@ -438,22 +453,38 @@ export const panel = (ui: Elements["terminal"], { span, body, master, detail, ov
438
453
  view.scroll = Math.max(0, Math.min(view.scroll, lines.length - body))
439
454
  const marks = scrollbar(body, lines.length, view.scroll)
440
455
  const blank = Text({ children: " " })
456
+ const gutter = solo ? spec.gutter ?? 0 : 0
457
+ const rail = spec.lines !== undefined && spec.fit !== true
458
+ const lead = gutter > 0 ? gutter + 2 : 2
459
+ const trail = rail ? 3 : 2
441
460
  const border = (left: string, joint: string, right: string) =>
442
- left + (spec.lines === undefined || solo ? "─".repeat(span + 1) :
443
- "─".repeat(master + 2) + joint + "─".repeat(detail + 4)) + right
444
- const scrolled = (i: number, width: number) => [
445
- Box({ width, flexShrink: 0, children:
446
- Text({ wrap: "truncate-end", dimColor: spec.dim ?? false,
447
- children: paint(Text, lines[view.scroll + i] ?? []) }) }),
448
- Text({ children: " " }),
449
- bar(Text, marks[i])
450
- ]
451
- const rows: RenderElement[] = [ edge(Text, border("╭", "┬", "╮"), spec.title) ]
461
+ left + (!rail ? "─".repeat(span + 1) :
462
+ (gutter > 0 ? `${"─".repeat(gutter)}${joint}${"─".repeat(span - 2 - gutter)}` :
463
+ solo ? "─".repeat(span - 1) :
464
+ `${"─".repeat(master + 2)}${joint}${"─".repeat(detail + 2)}`) + `${joint}─`) + right
465
+ const scrolled = (i: number, width: number) => {
466
+ const line = lines[view.scroll + i] ?? []
467
+ const fill = line[0]?.background
468
+ return [
469
+ ...(gutter > 0 ? [
470
+ Box({ width: gutter, flexShrink: 0, children: Text({ children: paint(Text, line.slice(0, gutter)) }) }),
471
+ Text({ ...BORDER, children: "│" }),
472
+ Text({ backgroundColor: fill, children: " " })
473
+ ] : []),
474
+ Box({ width, flexShrink: 0, children:
475
+ Text({ wrap: "truncate-end", dimColor: spec.dim ?? false,
476
+ children: paint(Text, line.slice(gutter)) }) }),
477
+ ...(rail ? [ Text({ backgroundColor: fill, children: " " }), Text({ ...BORDER, children: "│" }) ] :
478
+ [ Text({ children: " " }) ]),
479
+ bar(Text, marks[i])
480
+ ]
481
+ }
482
+ const rows: RenderElement[] = [ edge(Text, border("╭", "┬", "╮"), spec.title, "", lead, trail) ]
452
483
  for (let i = 0; i < body; i++) {
453
484
  const rule = !solo && spec.lines !== undefined && spec.rule?.(i) === true
454
485
  rows.push(Box({ flexDirection: "row", children: [
455
- Text({ ...BORDER, children: rule ? "├─" : "│ " }),
456
- ...(solo ? scrolled(i, span - 3) : spec.lines === undefined ? [
486
+ Text({ ...BORDER, children: rule ? "├─" : gutter > 0 ? "│" : "│ " }),
487
+ ...(solo ? scrolled(i, gutter > 0 ? span - 4 - gutter : span - 3) : spec.lines === undefined ? [
457
488
  Box({ width: span, flexShrink: 0, children: spec.rows!(i) ?? blank })
458
489
  ] : [
459
490
  rule ? Text({ ...BORDER, children: `${"─".repeat(master + 1)}┤ ` }) :
@@ -465,7 +496,7 @@ export const panel = (ui: Elements["terminal"], { span, body, master, detail, ov
465
496
  ] }))
466
497
  }
467
498
  rows.push(edge(Text, border("╰", "┴", "╯"), spec.status,
468
- spec.fit === true ? "" : position(body, lines.length, view.scroll)))
499
+ spec.fit === true ? "" : position(body, lines.length, view.scroll), lead, trail))
469
500
  rows.push(Box({ paddingLeft: 1, children: Text({ dimColor: true, wrap: "truncate-end", children: spec.hints }) }))
470
501
  if (overflow)
471
502
  rows.push(blank, blank)
@@ -29,7 +29,7 @@ const GROUP = /\[(?:[^[\]]|\[[^\]]*\])*\]|\((?:[^()]|\([^)]*\))*\)|\S+/g
29
29
  const NBSP = String.fromCodePoint(0xA0)
30
30
  const SYNTAX = "gray"
31
31
  const REFER = "red"
32
- const INDENT = 4
32
+ const INDENT = 2
33
33
  const TITLE = "[ ⧉ ASE: Show Skill Synopsis ]"
34
34
 
35
35
  /* a hint band: the scroll state of its frame, the skill command as typed,