@rse/ase 1.0.7 → 2.0.0
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-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +351 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +228 -111
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +3 -0
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +251 -76
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +225 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +79 -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-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
|
@@ -28,7 +28,6 @@ requested communication style at any time during a session.
|
|
|
28
28
|
- You *MUST* output blank lines between paragraphs.
|
|
29
29
|
|
|
30
30
|
- If <ase-persona-style/> is `engineer`, `journalist`, `telegrapher`, or `caveman`:
|
|
31
|
-
- You *MUST* use `○` as the bullet point marker symbol in bullet point lists.
|
|
32
31
|
- You *MUST* *drop* filler words
|
|
33
32
|
("just", "really", "basically", "actually", "simply", etc).
|
|
34
33
|
- You *MUST* *drop* pleasantries
|
|
@@ -51,7 +50,7 @@ requested communication style at any time during a session.
|
|
|
51
50
|
- You *MUST* *use only* one bullet point per explanation aspect.
|
|
52
51
|
- You *MUST* structure every bullet point by
|
|
53
52
|
following exactly the *pyramid* structure and formatting
|
|
54
|
-
|
|
53
|
+
`- **<title/>**: <core/> ▶ **<keywords/>**: <details/>`, where:
|
|
55
54
|
- <title/> is only one or two *words* capturing the aspect
|
|
56
55
|
- <core/> is a *very terse core* stating the essence of the aspect
|
|
57
56
|
using one of the following three sentence patterns:
|
|
@@ -81,7 +80,7 @@ requested communication style at any time during a session.
|
|
|
81
80
|
- If <ase-persona-style/> is `telegrapher`:
|
|
82
81
|
- You *MUST* structure every bullet point by
|
|
83
82
|
following exactly the *pyramid* structure and formatting
|
|
84
|
-
|
|
83
|
+
`- **<title/>**: <core/>`, where:
|
|
85
84
|
- <title/> is only one or two *words* capturing the aspect
|
|
86
85
|
- <core/> is a *very terse core* stating the essence of the aspect
|
|
87
86
|
using one of the following three sentence patterns:
|
|
@@ -93,7 +92,7 @@ requested communication style at any time during a session.
|
|
|
93
92
|
|
|
94
93
|
- If <ase-persona-style/> is `caveman`:
|
|
95
94
|
- You *MUST* structure every bullet point by
|
|
96
|
-
following exactly the formatting
|
|
95
|
+
following exactly the formatting `- <core/>`, where:
|
|
97
96
|
<core/> is a *very terse core* stating the essence of the aspect
|
|
98
97
|
using one of the following four sentence patterns:
|
|
99
98
|
- `<expression/>!`
|
package/plugin/meta/ase-skill.md
CHANGED
|
@@ -107,6 +107,15 @@ Skill Output
|
|
|
107
107
|
Skill Sequential Processing
|
|
108
108
|
---------------------------
|
|
109
109
|
|
|
110
|
+
- *IMPORTANT*: The task tracking rules below apply *only* if the
|
|
111
|
+
`TaskCreate` and `TaskUpdate` tools are actually available to you.
|
|
112
|
+
If they are *not* available (as with some agent tools or models),
|
|
113
|
+
silently skip *all* task tracking -- the creation of tasks, their
|
|
114
|
+
chaining via `addBlockedBy`, and their status updates -- and do
|
|
115
|
+
*not* search for these tools (e.g. via `ToolSearch`), do *not*
|
|
116
|
+
emulate them, and do *not* mention their absence. The strictly
|
|
117
|
+
sequential processing of every <step/> stays mandatory regardless.
|
|
118
|
+
|
|
110
119
|
- *IMPORTANT*: For each given <flow/>, you *MUST* use the
|
|
111
120
|
`TaskCreate` tool to create a corresponding set of processing steps.
|
|
112
121
|
|
|
@@ -0,0 +1,409 @@
|
|
|
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 { Register, RenderElement } from "claude-code"
|
|
16
|
+
|
|
17
|
+
/* the marker line above a grilling table (single-round and multi-round,
|
|
18
|
+
optionally labeled with the perspective and/or grounding) */
|
|
19
|
+
const MARKER = /⧉ \*{0,2}ASE\*{0,2}: GRILLING(?: ROUND \d+\/\d+)?(?: \([^)]*\))?:/
|
|
20
|
+
|
|
21
|
+
/* a row and the delimiter row of a Markdown table */
|
|
22
|
+
const ROW = /^ *\|.*\| *$/
|
|
23
|
+
const RULE = /^ *\|(?: *:?-+:? *\|)+ *$/
|
|
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 bare number of
|
|
128
|
+
another question (there is at most one selected question) is replaced in
|
|
129
|
+
place, else "n" is 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
|
+
if (letter === undefined && text.search(SELECT) !== -1)
|
|
139
|
+
return text.replace(SELECT, String(number))
|
|
140
|
+
const head = text.trimEnd()
|
|
141
|
+
return `${head}${head !== "" ? " " : ""}${number}${letter ?? ""}`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* convert inline Markdown (bold and code spans) into styled characters */
|
|
145
|
+
const style = (text: string, color: string | undefined): Char[] => {
|
|
146
|
+
const chars: Char[] = []
|
|
147
|
+
let bold = false
|
|
148
|
+
let code = false
|
|
149
|
+
for (let i = 0; i < text.length; i++) {
|
|
150
|
+
if (!code && text.startsWith("**", i)) {
|
|
151
|
+
bold = !bold
|
|
152
|
+
i++
|
|
153
|
+
}
|
|
154
|
+
else if (text[i] === "`")
|
|
155
|
+
code = !code
|
|
156
|
+
else {
|
|
157
|
+
const ch = String.fromCodePoint(text.codePointAt(i)!)
|
|
158
|
+
chars.push({ ch, bold, color: color ?? (code ? CODE : undefined) })
|
|
159
|
+
i += ch.length - 1
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return chars
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* the width of styled characters in terminal cells */
|
|
166
|
+
const width = (chars: Char[]): number => {
|
|
167
|
+
let n = 0
|
|
168
|
+
for (const { ch } of chars) {
|
|
169
|
+
const c = ch.codePointAt(0)!
|
|
170
|
+
if (c === 0x200B || c === 0x2060 || (c >= 0x0300 && c <= 0x036F) || (c >= 0xFE00 && c <= 0xFE0F))
|
|
171
|
+
continue
|
|
172
|
+
const wide =
|
|
173
|
+
(c >= 0x1100 && c <= 0x115F) || (c >= 0x2E80 && c <= 0xA4CF) ||
|
|
174
|
+
(c >= 0xAC00 && c <= 0xD7A3) || (c >= 0xF900 && c <= 0xFAFF) ||
|
|
175
|
+
(c >= 0xFF00 && c <= 0xFF60) || (c >= 0x1F300 && c <= 0x1FAFF)
|
|
176
|
+
n += wide ? 2 : 1
|
|
177
|
+
}
|
|
178
|
+
return n
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* hard-wrap styled characters at word boundaries to the given columns,
|
|
182
|
+
indenting the first line by "lead" and all further lines by "indent"
|
|
183
|
+
(and chopping overlong words) */
|
|
184
|
+
const wrap = (chars: Char[], columns: number, indent: number, lead = 0): Char[][] => {
|
|
185
|
+
const space: Char = { ch: " ", bold: false, color: undefined }
|
|
186
|
+
const words: Char[][] = [ [] ]
|
|
187
|
+
for (const char of chars) {
|
|
188
|
+
if (char.ch === " ")
|
|
189
|
+
words.push([])
|
|
190
|
+
else
|
|
191
|
+
words.at(-1)!.push(char)
|
|
192
|
+
}
|
|
193
|
+
const lines: Char[][] = []
|
|
194
|
+
let line: Char[] = Array.from({ length: lead }, () => space)
|
|
195
|
+
let start = lead
|
|
196
|
+
let size = lead
|
|
197
|
+
const flush = () => {
|
|
198
|
+
lines.push(line)
|
|
199
|
+
line = Array.from({ length: indent }, () => space)
|
|
200
|
+
start = indent
|
|
201
|
+
size = indent
|
|
202
|
+
}
|
|
203
|
+
for (let word of words.filter((word) => word.length > 0)) {
|
|
204
|
+
while (word.length > 0) {
|
|
205
|
+
const w = width(word)
|
|
206
|
+
if (size > start && size + 1 + w <= columns) {
|
|
207
|
+
line.push(space, ...word)
|
|
208
|
+
size += 1 + w
|
|
209
|
+
word = []
|
|
210
|
+
}
|
|
211
|
+
else if (size > start)
|
|
212
|
+
flush()
|
|
213
|
+
else {
|
|
214
|
+
let n = 1
|
|
215
|
+
while (n < word.length && width(word.slice(0, n + 1)) <= columns - start)
|
|
216
|
+
n++
|
|
217
|
+
line.push(...word.slice(0, n))
|
|
218
|
+
size += width(word.slice(0, n))
|
|
219
|
+
word = word.slice(n)
|
|
220
|
+
if (word.length > 0)
|
|
221
|
+
flush()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
lines.push(line)
|
|
226
|
+
return lines
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* function hook registration entry point */
|
|
230
|
+
export const register: Register = (on, options) => {
|
|
231
|
+
/* allow the rendering to be disabled without disabling the plugin */
|
|
232
|
+
if (options.enabled === false)
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
/* the last known width of the terminal, the already seen tables, the
|
|
236
|
+
latest (still unanswered) table, the picks and the selected question of the
|
|
237
|
+
still unsubmitted prompt, and the frozen picks of the already answered tables */
|
|
238
|
+
let columns = 80
|
|
239
|
+
const seen = new Set<string>()
|
|
240
|
+
let latest: string | undefined
|
|
241
|
+
let picks = new Map<number, string>()
|
|
242
|
+
let selected: number | undefined
|
|
243
|
+
const frozen = new Map<string, Map<number, string>>()
|
|
244
|
+
|
|
245
|
+
/* track the picks inside the still unsubmitted prompt of the user */
|
|
246
|
+
on("prompt.edit", async ($, e, next) => {
|
|
247
|
+
const box = await next(e)
|
|
248
|
+
const now = pick(box.text)
|
|
249
|
+
const sel = select(box.text)
|
|
250
|
+
if (JSON.stringify([ ...now ]) !== JSON.stringify([ ...picks ]) || sel !== selected) {
|
|
251
|
+
picks = now
|
|
252
|
+
selected = sel
|
|
253
|
+
$.ui.invalidate("ui.render")
|
|
254
|
+
}
|
|
255
|
+
return box
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
/* freeze the picks in the latest table once the prompt is submitted */
|
|
259
|
+
on("prompt.submit", ($, e, next) => {
|
|
260
|
+
if (latest !== undefined)
|
|
261
|
+
frozen.set(latest, pick(e.text))
|
|
262
|
+
latest = undefined
|
|
263
|
+
picks = new Map()
|
|
264
|
+
selected = undefined
|
|
265
|
+
$.ui.invalidate("ui.render")
|
|
266
|
+
return next(e)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
/* redraw the replies which contain a grilling table (terminal only) */
|
|
270
|
+
on("ui.render", { component: "AssistantMessage", surface: "terminal" }, async ($, e, next) => {
|
|
271
|
+
/* act on grilling tables only, which fit into the terminal
|
|
272
|
+
(gutter, grid lines, cell paddings, and a spare column subtracted) */
|
|
273
|
+
columns = e.viewport?.columns ?? columns
|
|
274
|
+
const table = parse(e.props.text)
|
|
275
|
+
const avail = columns - 2 - 7 - 1
|
|
276
|
+
if (table === undefined || avail < 40 || table.before.length > LIMIT || table.after.length > LIMIT)
|
|
277
|
+
return next(e)
|
|
278
|
+
|
|
279
|
+
/* let the chain beneath run, although its drawing is replaced by us
|
|
280
|
+
(but this at least silences a warning in the debug output of Claude Code) */
|
|
281
|
+
await next(e)
|
|
282
|
+
|
|
283
|
+
/* determine the picks: frozen ones or, for the latest table, the live ones */
|
|
284
|
+
if (!seen.has(e.requestId)) {
|
|
285
|
+
seen.add(e.requestId)
|
|
286
|
+
latest = e.requestId
|
|
287
|
+
}
|
|
288
|
+
const chosen = frozen.get(e.requestId) ?? (e.requestId === latest ? picks : undefined)
|
|
289
|
+
const marked = e.requestId === latest ? selected : undefined
|
|
290
|
+
|
|
291
|
+
/* style the cells and determine the column widths, right-aligning the
|
|
292
|
+
question numbers to the widest one (so the wrapped lines of all
|
|
293
|
+
questions share the same indent) */
|
|
294
|
+
const head = table.header.map((cell) => style(`**${cell}**`, undefined))
|
|
295
|
+
const digits = Math.max(...table.rows.map((row) => String(row.number).length))
|
|
296
|
+
const rows = table.rows.map((row) => {
|
|
297
|
+
const numbered = NUMBER.test(row.question)
|
|
298
|
+
return {
|
|
299
|
+
lead: numbered ? digits - String(row.number).length : 0,
|
|
300
|
+
indent: numbered ? digits + 3 : 0,
|
|
301
|
+
question: style(row.question, marked === row.number ? PICKED : undefined),
|
|
302
|
+
answers: row.answers.map((answer) =>
|
|
303
|
+
style(answer.text, chosen?.get(row.number) === answer.letter ? PICKED : undefined))
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
const w1nat = Math.max(width(head[0]!), ...rows.map((row) => row.lead + width(row.question)))
|
|
307
|
+
const w2nat = Math.max(width(head[1]!), ...rows.flatMap((row) => row.answers.map((answer) => width(answer))))
|
|
308
|
+
let w1 = Math.min(w1nat, Math.floor(avail * 0.4))
|
|
309
|
+
const w2 = Math.min(w2nat, avail - w1)
|
|
310
|
+
w1 = Math.min(w1nat, avail - w2)
|
|
311
|
+
|
|
312
|
+
/* resolve the UI elements */
|
|
313
|
+
const { Box, Text, Button, Markdown } = $.ui.resolve(e)
|
|
314
|
+
|
|
315
|
+
/* split a line of a cell into runs of equally styled characters */
|
|
316
|
+
const split = (chars: Char[]): { text: string, bold: boolean, color: string | undefined }[] => {
|
|
317
|
+
const runs: { text: string, bold: boolean, color: string | undefined }[] = []
|
|
318
|
+
for (let i = 0, j = 0; i < chars.length; i = j) {
|
|
319
|
+
const { bold, color } = chars[i]!
|
|
320
|
+
while (j < chars.length && chars[j]!.bold === bold && chars[j]!.color === color)
|
|
321
|
+
j++
|
|
322
|
+
runs.push({ text: chars.slice(i, j).map((char) => char.ch).join(""), bold, color })
|
|
323
|
+
}
|
|
324
|
+
return runs
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* draw a line of a cell: its styled runs, padded to the column width */
|
|
328
|
+
const paint = (chars: Char[], columns: number): (string | RenderElement)[] => [
|
|
329
|
+
...split(chars).map(({ text, bold, color }) =>
|
|
330
|
+
bold || color !== undefined ? Text({ bold, color, children: text }) : text),
|
|
331
|
+
" ".repeat(Math.max(0, columns - width(chars)))
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
/* insert the number of a clicked question or the short response of a clicked
|
|
335
|
+
answer into the prompt and refresh the picks and the selection from it */
|
|
336
|
+
const press = (number: number, letter?: string) => () => {
|
|
337
|
+
void $.prompt.read()
|
|
338
|
+
.then(({ text }) => $.prompt.fill({ text: insert(text, number, letter) }))
|
|
339
|
+
.then((box) => {
|
|
340
|
+
picks = pick(box.text)
|
|
341
|
+
selected = select(box.text)
|
|
342
|
+
$.ui.invalidate("ui.render")
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* draw a line of a cell: in the latest table as the styled text inside a
|
|
347
|
+
hover-scoping box, which while any line of its group (all lines of the
|
|
348
|
+
same question or answer) is hovered reveals a row of clickable plain
|
|
349
|
+
buttons (one per styled run, inversed in the default colors and carrying
|
|
350
|
+
the run's bold style, but not its color, as its hover style, because the
|
|
351
|
+
engine leaves the space cells uncolored) on top of it; otherwise as
|
|
352
|
+
styled text only */
|
|
353
|
+
const draw = (chars: Char[], columns: number, group: string, line: number,
|
|
354
|
+
onPress: () => void): RenderElement[] => {
|
|
355
|
+
const w = width(chars)
|
|
356
|
+
if (e.requestId !== latest || w === 0)
|
|
357
|
+
return [ Text({ children: paint(chars, columns) }) ]
|
|
358
|
+
const buttons = split(chars).map(({ text, bold }, r) =>
|
|
359
|
+
Button({ key: `${group}/${line}/${r}`, label: text, plain: true, onPress,
|
|
360
|
+
hover: { scope: group, inverse: true, bold } }))
|
|
361
|
+
return [
|
|
362
|
+
Box({ key: `${group}/${line}`, hover: { scope: group }, children: [
|
|
363
|
+
Text({ children: paint(chars, w) }),
|
|
364
|
+
Box({ position: "absolute", top: 0, left: 0, display: "none",
|
|
365
|
+
hover: { scope: group, display: "flex" }, flexDirection: "row", children: buttons })
|
|
366
|
+
] }),
|
|
367
|
+
Text({ children: " ".repeat(Math.max(0, columns - w)) })
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* draw the table as a grid: a rule between all rows and one answer per line */
|
|
372
|
+
const rule = (l: string, m: string, r: string) =>
|
|
373
|
+
Text({ wrap: "truncate-end", children: l + "─".repeat(w1 + 2) + m + "─".repeat(w2 + 2) + r })
|
|
374
|
+
const grid: RenderElement[] = [ rule("┌", "┬", "┐") ]
|
|
375
|
+
grid.push(Text({ wrap: "truncate-end", children:
|
|
376
|
+
[ "│ ", ...paint(head[0]!, w1), " │ ", ...paint(head[1]!, w2), " │" ] }))
|
|
377
|
+
grid.push(rule("├", "┼", "┤"))
|
|
378
|
+
rows.forEach((row, k) => {
|
|
379
|
+
const number = table.rows[k]!.number
|
|
380
|
+
const q = wrap(row.question, w1, row.indent, row.lead).map((line, i) =>
|
|
381
|
+
draw(line, w1, `q${number}`, i, press(number)))
|
|
382
|
+
const a = row.answers.flatMap((answer, j) => {
|
|
383
|
+
const letter = table.rows[k]!.answers[j]!.letter
|
|
384
|
+
return wrap(answer, w2, INDENT).map((line, i) =>
|
|
385
|
+
draw(line, w2, `a${number}${letter}`, i, press(number, letter)))
|
|
386
|
+
})
|
|
387
|
+
for (let i = 0; i < Math.max(q.length, a.length); i++)
|
|
388
|
+
grid.push(Box({ flexDirection: "row", children: [
|
|
389
|
+
Text({ children: "│ " }),
|
|
390
|
+
...(q[i] ?? [ Text({ children: " ".repeat(w1) }) ]),
|
|
391
|
+
Text({ children: " │ " }),
|
|
392
|
+
...(a[i] ?? [ Text({ children: " ".repeat(w2) }) ]),
|
|
393
|
+
Text({ children: " │" })
|
|
394
|
+
] }))
|
|
395
|
+
grid.push(k < rows.length - 1 ? rule("├", "┼", "┤") : rule("└", "┴", "┘"))
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
/* mimic the reply row of the engine: blank row above and bullet gutter */
|
|
399
|
+
return Box({ flexDirection: "row", marginTop: 1, width: "100%", children: [
|
|
400
|
+
Box({ flexShrink: 0, minWidth: 2, children: Text({ children: e.props.isFirstOfReply ? "⏺" : " " }) }),
|
|
401
|
+
Box({ flexDirection: "column", flexGrow: 1, flexShrink: 1, children: [
|
|
402
|
+
...(table.before !== "" ? [ Markdown({ text: table.before }) ] : []),
|
|
403
|
+
Box({ flexDirection: "column", marginTop: table.before !== "" ? 1 : 0, children: grid }),
|
|
404
|
+
...(table.after !== "" ? [ Box({ marginTop: 1, children: Markdown({ text: table.after }) }) ] : [])
|
|
405
|
+
] })
|
|
406
|
+
] })
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
|