@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.
Files changed (279) hide show
  1. package/dst/ase-agent-definition-api.d.ts +184 -0
  2. package/dst/ase-agent-definition-api.js +6 -0
  3. package/dst/ase-agent-definition-builtin/condenser.js +42 -0
  4. package/dst/ase-agent-definition-builtin/editor.js +45 -0
  5. package/dst/ase-agent-definition-builtin/estimator.js +98 -0
  6. package/dst/ase-agent-definition-builtin/implementer.js +121 -0
  7. package/dst/ase-agent-definition-builtin/integrator.js +56 -0
  8. package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
  9. package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
  10. package/dst/ase-agent-definition-builtin/researcher.js +77 -0
  11. package/dst/ase-agent-definition-core.js +827 -0
  12. package/dst/ase-agent-fleet.js +772 -0
  13. package/dst/ase-agent-process.js +232 -0
  14. package/dst/ase-agent-relay-client.js +509 -0
  15. package/dst/ase-agent-relay-core.js +213 -0
  16. package/dst/ase-agent-relay-server-bind.js +242 -0
  17. package/dst/ase-agent-relay-server-cli.js +50 -0
  18. package/dst/ase-agent-runtime-claim.js +189 -0
  19. package/dst/ase-agent-runtime-engine.js +1100 -0
  20. package/dst/ase-agent-runtime-exec.js +275 -0
  21. package/dst/ase-agent-runtime-phase.js +581 -0
  22. package/dst/ase-agent-runtime-request.js +272 -0
  23. package/dst/ase-agent-runtime-tool.js +207 -0
  24. package/dst/ase-agent-runtime.js +1316 -0
  25. package/dst/ase-agent.js +65 -0
  26. package/dst/ase-artifact.js +6 -5
  27. package/dst/ase-config-core.js +465 -0
  28. package/dst/ase-config-schema.js +351 -0
  29. package/dst/ase-config-scope.js +172 -0
  30. package/dst/ase-config.js +25 -585
  31. package/dst/ase-hook.js +228 -111
  32. package/dst/ase-lib-daemon-cli.js +130 -0
  33. package/dst/ase-lib-hash.js +20 -0
  34. package/dst/{ase-log.js → ase-lib-log.js} +40 -7
  35. package/dst/ase-lib-server-bind.js +115 -0
  36. package/dst/ase-lib-server-cli.js +474 -0
  37. package/dst/{ase-table.js → ase-lib-table.js} +1 -1
  38. package/dst/{ase-version.js → ase-lib-version.js} +3 -0
  39. package/dst/ase-man.js +96 -0
  40. package/dst/ase-mcp.js +166 -13
  41. package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
  42. package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
  43. package/dst/ase-service-otel.js +1246 -0
  44. package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
  45. package/dst/ase-service-state.js +101 -0
  46. package/dst/ase-service.js +251 -76
  47. package/dst/ase-setup-common.js +141 -0
  48. package/dst/ase-setup-mcp.js +359 -0
  49. package/dst/ase-setup-settings.js +837 -0
  50. package/dst/ase-setup.js +113 -869
  51. package/dst/ase-spec.js +31 -19
  52. package/dst/ase-statusline.js +108 -43
  53. package/dst/ase-task-board-core.js +739 -0
  54. package/dst/ase-task-board-filter.js +127 -0
  55. package/dst/ase-task-board-flow.js +237 -0
  56. package/dst/ase-task-board-graph.js +497 -0
  57. package/dst/ase-task-board-stats.js +263 -0
  58. package/dst/ase-task-board-tui-control.js +775 -0
  59. package/dst/ase-task-board-tui-model.js +1008 -0
  60. package/dst/ase-task-board-tui-popup.js +626 -0
  61. package/dst/ase-task-board-tui-style.js +106 -0
  62. package/dst/ase-task-board-tui-view.js +847 -0
  63. package/dst/ase-task-board-tui.js +189 -0
  64. package/dst/ase-task-board-web-client.css +1 -0
  65. package/dst/ase-task-board-web-client.html +20 -0
  66. package/dst/ase-task-board-web-client.js +81 -0
  67. package/dst/ase-task-board-web-server.js +823 -0
  68. package/dst/ase-task-board.js +134 -0
  69. package/dst/ase-task-format.js +757 -0
  70. package/dst/ase-task-request.js +208 -0
  71. package/dst/ase-task-store-client.js +495 -0
  72. package/dst/ase-task-store-core.js +804 -0
  73. package/dst/ase-task-store-issues.js +237 -0
  74. package/dst/ase-task-store-plugin-api.js +6 -0
  75. package/dst/ase-task-store-plugin-ase.js +484 -0
  76. package/dst/ase-task-store-plugin-delegate.js +137 -0
  77. package/dst/ase-task-store-plugin-gitea.js +666 -0
  78. package/dst/ase-task-store-plugin-github.js +696 -0
  79. package/dst/ase-task-store-plugin-gitlab.js +668 -0
  80. package/dst/ase-task-store-server-bind.js +538 -0
  81. package/dst/ase-task-store-server-cli.js +154 -0
  82. package/dst/ase-task.js +1335 -357
  83. package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
  84. package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
  85. package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
  86. package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
  87. package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
  88. package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
  89. package/dst/ase-util.js +20 -0
  90. package/dst/ase.js +108 -32
  91. package/dst/man/ase-agent-adhoc.1 +515 -0
  92. package/dst/man/ase-agent-condenser.1 +193 -0
  93. package/dst/man/ase-agent-editor.1 +212 -0
  94. package/dst/man/ase-agent-estimator.1 +214 -0
  95. package/dst/man/ase-agent-fleet.1 +185 -0
  96. package/dst/man/ase-agent-implementer.1 +233 -0
  97. package/dst/man/ase-agent-integrator.1 +213 -0
  98. package/dst/man/ase-agent-qualifier.1 +214 -0
  99. package/dst/man/ase-agent-rebooter.1 +193 -0
  100. package/dst/man/ase-agent-relay.1 +128 -0
  101. package/dst/man/ase-agent-researcher.1 +217 -0
  102. package/dst/man/ase-agent.1 +264 -0
  103. package/dst/man/ase-artifact-list.1 +38 -0
  104. package/dst/man/ase-artifact-name.1 +39 -0
  105. package/dst/man/ase-artifact.1 +56 -0
  106. package/dst/man/ase-config-delete.1 +45 -0
  107. package/dst/man/ase-config-edit.1 +48 -0
  108. package/dst/man/ase-config-get.1 +46 -0
  109. package/dst/man/ase-config-init.1 +60 -0
  110. package/dst/man/ase-config-list.1 +42 -0
  111. package/dst/man/ase-config-set.1 +52 -0
  112. package/dst/man/ase-config.1 +225 -0
  113. package/dst/man/ase-hook-permission-request.1 +36 -0
  114. package/dst/man/ase-hook-pre-tool-use.1 +59 -0
  115. package/dst/man/ase-hook-session-end.1 +47 -0
  116. package/dst/man/ase-hook-session-persona.1 +37 -0
  117. package/dst/man/ase-hook-session-start.1 +81 -0
  118. package/dst/man/ase-hook-stop.1 +34 -0
  119. package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
  120. package/dst/man/ase-hook.1 +51 -0
  121. package/dst/man/ase-man.1 +67 -0
  122. package/dst/man/ase-mcp.1 +61 -0
  123. package/dst/man/ase-service-log.1 +43 -0
  124. package/dst/man/ase-service-send.1 +47 -0
  125. package/dst/man/ase-service-start.1 +54 -0
  126. package/dst/man/ase-service-status.1 +46 -0
  127. package/dst/man/ase-service-stop.1 +37 -0
  128. package/dst/man/ase-service.1 +85 -0
  129. package/dst/man/ase-setup-disable.1 +44 -0
  130. package/dst/man/ase-setup-enable.1 +45 -0
  131. package/dst/man/ase-setup-install.1 +73 -0
  132. package/dst/man/ase-setup-mcp.1 +124 -0
  133. package/dst/man/ase-setup-status.1 +60 -0
  134. package/dst/man/ase-setup-statusline.1 +92 -0
  135. package/dst/man/ase-setup-telemetry.1 +92 -0
  136. package/dst/man/ase-setup-uninstall.1 +59 -0
  137. package/dst/man/ase-setup-update.1 +64 -0
  138. package/dst/man/ase-setup.1 +93 -0
  139. package/dst/man/ase-spec-export.1 +62 -0
  140. package/dst/man/ase-spec-lint.1 +40 -0
  141. package/dst/man/ase-spec-preview.1 +40 -0
  142. package/dst/man/ase-spec.1 +46 -0
  143. package/dst/man/ase-statusline.1 +189 -0
  144. package/dst/man/ase-task-assign.1 +71 -0
  145. package/dst/man/ase-task-board.1 +384 -0
  146. package/dst/man/ase-task-delete.1 +35 -0
  147. package/dst/man/ase-task-edit.1 +46 -0
  148. package/dst/man/ase-task-idscheme.1 +65 -0
  149. package/dst/man/ase-task-lifecycle.1 +57 -0
  150. package/dst/man/ase-task-list.1 +47 -0
  151. package/dst/man/ase-task-load.1 +36 -0
  152. package/dst/man/ase-task-newid.1 +61 -0
  153. package/dst/man/ase-task-purge.1 +37 -0
  154. package/dst/man/ase-task-rename.1 +44 -0
  155. package/dst/man/ase-task-request.1 +61 -0
  156. package/dst/man/ase-task-save.1 +48 -0
  157. package/dst/man/ase-task-status.1 +55 -0
  158. package/dst/man/ase-task-store.1 +194 -0
  159. package/dst/man/ase-task-view.1 +40 -0
  160. package/dst/man/ase-task.1 +160 -0
  161. package/dst/man/ase-util-compat.1 +25 -0
  162. package/dst/man/ase-util-diagram.1 +76 -0
  163. package/dst/man/ase-util-meta.1 +45 -0
  164. package/dst/man/ase-util-metric.1 +54 -0
  165. package/dst/man/ase-util-mint.1 +59 -0
  166. package/dst/man/ase-util-worktree.1 +72 -0
  167. package/dst/man/ase-util.1 +50 -0
  168. package/dst/man/ase.1 +124 -0
  169. package/package.json +79 -21
  170. package/plugin/.claude-plugin/plugin.json +1 -1
  171. package/plugin/.codex-plugin/plugin.json +1 -1
  172. package/plugin/.github/plugin/plugin.json +1 -1
  173. package/plugin/agents/ase-meta-diagram.md +12 -8
  174. package/plugin/agents/ase-repo-review.md +322 -0
  175. package/plugin/hooks/hooks-codex.json +5 -0
  176. package/plugin/hooks/hooks-copilot.json +5 -0
  177. package/plugin/hooks/hooks.json +8 -0
  178. package/plugin/meta/ase-common-approach.md +117 -0
  179. package/plugin/meta/ase-common-changeset.md +256 -0
  180. package/plugin/meta/ase-common-code.md +10 -173
  181. package/plugin/meta/ase-common-grill.md +317 -0
  182. package/plugin/meta/ase-common-resolve.md +305 -0
  183. package/plugin/meta/ase-common-task.md +83 -4
  184. package/plugin/meta/ase-constitution.md +0 -2
  185. package/plugin/meta/ase-format-specbook.md +208 -25
  186. package/plugin/meta/ase-format-specbook.yaml +242 -851
  187. package/plugin/meta/ase-format-task.md +669 -125
  188. package/plugin/meta/ase-persona.md +3 -4
  189. package/plugin/meta/ase-skill.md +9 -0
  190. package/plugin/mods/ase-mods.ts +409 -0
  191. package/plugin/mods/claude-code.d.ts +13417 -0
  192. package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
  193. package/plugin/skills/ase-arch-analyze/help.md +2 -2
  194. package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
  195. package/plugin/skills/ase-code-analyze/help.md +29 -4
  196. package/plugin/skills/ase-code-edit/SKILL.md +107 -181
  197. package/plugin/skills/ase-code-edit/help.md +118 -31
  198. package/plugin/skills/ase-code-lint/SKILL.md +64 -13
  199. package/plugin/skills/ase-code-lint/help.md +32 -2
  200. package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
  201. package/plugin/skills/ase-docs-proofread/help.md +1 -1
  202. package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
  203. package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
  204. package/plugin/skills/ase-help-intent/data.md +2164 -1087
  205. package/plugin/skills/ase-help-intent/help.md +1 -1
  206. package/plugin/skills/ase-help-skill/catalog.md +12 -9
  207. package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
  208. package/plugin/skills/ase-meta-config/SKILL.md +3 -4
  209. package/plugin/skills/ase-meta-config/help.md +31 -10
  210. package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
  211. package/plugin/skills/ase-meta-mint/help.md +10 -5
  212. package/plugin/skills/ase-meta-why/help.md +1 -1
  213. package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
  214. package/plugin/skills/ase-meta-workflow/help.md +4 -4
  215. package/plugin/skills/ase-meta-workflow/sample.md +3 -3
  216. package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
  217. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
  218. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
  219. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
  220. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
  221. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
  222. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
  223. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
  224. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
  225. package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
  226. package/plugin/skills/ase-repo-merge/help.md +107 -0
  227. package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
  228. package/plugin/skills/ase-repo-resolve/help.md +102 -0
  229. package/plugin/skills/ase-repo-review/SKILL.md +406 -0
  230. package/plugin/skills/ase-repo-review/help.md +181 -0
  231. package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
  232. package/plugin/skills/ase-spec-edit/help.md +107 -26
  233. package/plugin/skills/ase-sync-reconcile/help.md +1 -1
  234. package/plugin/skills/ase-task-assign/SKILL.md +111 -0
  235. package/plugin/skills/ase-task-assign/help.md +119 -0
  236. package/plugin/skills/ase-task-condense/SKILL.md +21 -21
  237. package/plugin/skills/ase-task-condense/help.md +12 -10
  238. package/plugin/skills/ase-task-delete/SKILL.md +1 -1
  239. package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
  240. package/plugin/skills/ase-task-dissect/help.md +10 -9
  241. package/plugin/skills/ase-task-edit/SKILL.md +379 -94
  242. package/plugin/skills/ase-task-edit/help.md +95 -22
  243. package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
  244. package/plugin/skills/ase-task-estimate/help.md +158 -0
  245. package/plugin/skills/ase-task-grill/SKILL.md +246 -138
  246. package/plugin/skills/ase-task-grill/help.md +129 -26
  247. package/plugin/skills/ase-task-id/SKILL.md +8 -1
  248. package/plugin/skills/ase-task-id/help.md +6 -3
  249. package/plugin/skills/ase-task-implement/SKILL.md +535 -145
  250. package/plugin/skills/ase-task-implement/help.md +138 -47
  251. package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
  252. package/plugin/skills/ase-task-integrate/help.md +187 -0
  253. package/plugin/skills/ase-task-list/SKILL.md +55 -23
  254. package/plugin/skills/ase-task-list/help.md +45 -21
  255. package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
  256. package/plugin/skills/ase-task-reboot/help.md +28 -8
  257. package/plugin/skills/ase-task-rename/SKILL.md +10 -2
  258. package/plugin/skills/ase-task-research/SKILL.md +386 -0
  259. package/plugin/skills/ase-task-research/help.md +102 -0
  260. package/plugin/skills/ase-task-status/SKILL.md +137 -0
  261. package/plugin/skills/ase-task-status/help.md +80 -0
  262. package/plugin/skills/ase-task-view/SKILL.md +49 -24
  263. package/plugin/skills/ase-task-view/help.md +19 -9
  264. package/plugin/agents/ase-meta-review.md +0 -187
  265. package/plugin/skills/ase-code-craft/SKILL.md +0 -264
  266. package/plugin/skills/ase-code-craft/help.md +0 -122
  267. package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
  268. package/plugin/skills/ase-code-refactor/help.md +0 -109
  269. package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
  270. package/plugin/skills/ase-code-resolve/help.md +0 -116
  271. package/plugin/skills/ase-meta-review/SKILL.md +0 -210
  272. package/plugin/skills/ase-meta-review/help.md +0 -96
  273. package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
  274. package/plugin/skills/ase-task-preflight/help.md +0 -82
  275. /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
  276. /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
  277. /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
  278. /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
  279. /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
@@ -0,0 +1,626 @@
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
+ import { Box, Text } from "ink";
7
+ import { cardLabel, groupLabel, attachmentTabs, isDiff, diffTones, dataLang, dataSpans, clampLines } from "./ase-task-board-core.js";
8
+ import { h } from "./ase-task-board-tui-view.js";
9
+ import { palette, cx, spinnerFrames, eighths } from "./ase-task-board-tui-style.js";
10
+ const BOLD = 1;
11
+ const ITALIC = 2;
12
+ const CODE = 4;
13
+ const ACCENT = 8;
14
+ const SIGNAL = 16;
15
+ const DIM = 32;
16
+ /* strip the Markdown inline markers of a line ("`" for code, "**" and
17
+ "__" for bold, "*" for italic), recording the style of the marked text */
18
+ const emphasis = (line) => {
19
+ const out = { text: "", mask: [] };
20
+ const add = (text, style) => {
21
+ const n = out.mask.length;
22
+ out.text += text;
23
+ out.mask.length = n + text.length;
24
+ out.mask.fill(style, n);
25
+ };
26
+ let last = 0;
27
+ for (const m of line.matchAll(/`([^`]+)`|\*\*(.+?)\*\*|__(.+?)__|\*([^*\s](?:[^*]*?[^*\s])?)\*/g)) {
28
+ add(line.slice(last, m.index), 0);
29
+ add(m[1] ?? m[2] ?? m[3] ?? m[4], m[1] !== undefined ? CODE : m[4] !== undefined ? ITALIC : BOLD);
30
+ last = m.index + m[0].length;
31
+ }
32
+ add(line.slice(last), 0);
33
+ return out;
34
+ };
35
+ /* wrap a text line to a width, keeping its leading indentation */
36
+ const wrap = (line, width) => {
37
+ if (line.text.length <= width)
38
+ return [line];
39
+ const indent = /^\s*(?:(?:[-*]|\d+[.)])\s+(?:\[.\]\s+)?)?/.exec(line.text)[0].length;
40
+ const pad = " ".repeat(Math.min(indent, Math.floor(width / 2)));
41
+ const out = [];
42
+ let rest = line;
43
+ while (rest.text.length > width) {
44
+ let cut = rest.text.lastIndexOf(" ", width);
45
+ if (cut <= pad.length)
46
+ cut = width;
47
+ out.push({ text: rest.text.slice(0, cut), mask: rest.mask.slice(0, cut) });
48
+ const tail = rest.text.slice(cut);
49
+ const skip = tail.length - tail.trimStart().length;
50
+ rest = {
51
+ text: pad + tail.slice(skip),
52
+ mask: [...new Array(pad.length).fill(0), ...rest.mask.slice(cut + skip)]
53
+ };
54
+ }
55
+ out.push(rest);
56
+ return out;
57
+ };
58
+ /* make a text safe for the terminal: expand tabs and strip control characters */
59
+ export const sanitize = (text) => text.replace(/\t/g, " ").replace(/[\p{Cc}؜‎‏‪-‮⁦-⁩]/gu, "");
60
+ /* mark the checkbox of a plan line in the read dialog: checkboxes of items
61
+ which are done, in progress, or open are accented, while checkboxes of
62
+ items which are questioned, delegated, or dropped are signaled */
63
+ const checkbox = (line) => {
64
+ const m = /^(\s*(?:[-*]|\d+[.)])\s+)\[([x /?>-])\]/.exec(line.text);
65
+ if (m !== null) {
66
+ const style = /[x/ ]/.test(m[2]) ? ACCENT : SIGNAL;
67
+ for (let k = m[1].length; k < m[1].length + 3; k++)
68
+ line.mask[k] |= style;
69
+ }
70
+ return line;
71
+ };
72
+ /* the key/value header lines of the read dialog (each cut to the width with
73
+ an ellipsis), separated from the content by a line, so that the content
74
+ starts directly after it, resp. (for Markdown content) after a blank line */
75
+ const headerLines = (keys, width, blank) => {
76
+ const out = keys.map(([key, val]) => {
77
+ const text = `${(key + ":").padEnd(11)}${sanitize(val)}`;
78
+ return { text: text.length > width ? text.slice(0, Math.max(0, width - 1)) + "…" : text, color: palette.dim };
79
+ });
80
+ if (out.length > 0) {
81
+ out.push({ text: "─".repeat(width), color: palette.dim });
82
+ if (blank)
83
+ out.push({ text: "" });
84
+ }
85
+ return out;
86
+ };
87
+ /* the lines of a Markdown text (without its leading blank lines) for the read dialog */
88
+ const markdownLines = (text, width) => {
89
+ const out = [];
90
+ for (const line of text.replace(/^(?:[ \t]*\r?\n)+/, "").split(/\r?\n/).map(sanitize)) {
91
+ const heading = /^(#{1,6})[ \t]+/.exec(line);
92
+ const bold = heading !== null;
93
+ const color = heading !== null && heading[1].length <= 4 ? palette.accent : palette.normal;
94
+ for (const part of wrap(checkbox(emphasis(heading !== null ? line.slice(heading[0].length) : line)), width))
95
+ out.push({ text: part.text, color, bold, mask: part.mask });
96
+ }
97
+ return out;
98
+ };
99
+ /* the lines of a task plan for the read dialog */
100
+ export const planLines = (parts, id, width) => {
101
+ if (parts === undefined)
102
+ return [{ text: "loading...", color: palette.dim }];
103
+ if (parts === null)
104
+ return [{ text: `task "${id}" no longer exists`, color: palette.signal }];
105
+ if (parts instanceof Error)
106
+ return [{ text: `loading task "${id}" failed: ${parts.message}`, color: palette.signal }];
107
+ return [
108
+ ...headerLines([...parts.keys].filter(([key]) => key !== "Type"), width, true),
109
+ ...markdownLines(parts.body, width)
110
+ ];
111
+ };
112
+ /* the lines of a task plan attachment for the read dialog: its embedded
113
+ data or its (lazily loaded) file content, rendered as Markdown for a
114
+ Markdown type, syntax-highlighted for a YAML or JSON type, colored per
115
+ line for a diff, as a placeholder if binary, and else verbatim */
116
+ export const attachmentLines = (att, content, width) => {
117
+ const markdown = /^text\/markdown\b/i.test(att.type.trim());
118
+ const out = headerLines([
119
+ ["Type", att.type], ["Desc", att.desc], ["Created", att.created ?? ""], ["Modified", att.modified ?? ""]
120
+ ].filter(([, val]) => val !== ""), width, markdown);
121
+ let text = att.data;
122
+ if (text === undefined) {
123
+ if (content === undefined)
124
+ return [...out, { text: "loading...", color: palette.dim }];
125
+ if (content instanceof Error)
126
+ return [...out, { text: `loading attachment failed: ${content.message}`, color: palette.signal }];
127
+ try {
128
+ text = new TextDecoder("utf-8", { fatal: true }).decode(content);
129
+ }
130
+ catch (_err) {
131
+ text = "\0";
132
+ }
133
+ if (text.includes("\0"))
134
+ return [...out, { text: `(binary content: ${sanitize(att.type)}, ${content.length} bytes)`, color: palette.dim }];
135
+ }
136
+ if (markdown)
137
+ return [...out, ...markdownLines(text, width)];
138
+ /* YAML or JSON data is syntax-highlighted: keys accented, numbers and
139
+ literals signaled, indicators and comments dimmed (comments also
140
+ italic), and strings in the default color */
141
+ const lang = dataLang(att.type);
142
+ if (lang !== null) {
143
+ const style = { key: ACCENT, num: SIGNAL, lit: SIGNAL, punct: DIM, cmt: DIM | ITALIC };
144
+ for (const spans of dataSpans(text.replace(/^(?:[ \t]*\r?\n)+/, ""), lang)) {
145
+ const line = { text: "", mask: [] };
146
+ for (const span of spans) {
147
+ const part = sanitize(span.text);
148
+ line.text += part;
149
+ line.mask.push(...new Array(part.length).fill(style[span.tone] ?? 0));
150
+ }
151
+ for (const part of wrap(line, width))
152
+ out.push({ text: part.text, color: palette.normal, mask: part.mask });
153
+ }
154
+ return out;
155
+ }
156
+ /* a diff is colored per line: its file headers up to and
157
+ including the hunk headers dimmed, inserted lines accented, removed
158
+ lines signaled, and context lines in the default color */
159
+ const lines = text.replace(/^(?:[ \t]*\r?\n)+/, "").split(/\r?\n/).map(sanitize);
160
+ const tones = isDiff(att.type) ? diffTones(lines) : null;
161
+ const tint = { head: palette.dim, add: palette.accent, del: palette.signal, ctx: palette.normal };
162
+ lines.forEach((line, i) => {
163
+ const color = tones !== null ? tint[tones[i]] : palette.accent;
164
+ for (const part of wrap({ text: line, mask: new Array(line.length).fill(0) }, width))
165
+ out.push({ text: part.text, color, mask: part.mask });
166
+ });
167
+ return out;
168
+ };
169
+ /* the full local timestamp of a processing log line as "YYYY-MM-DD HH:MM:SS.mmm" */
170
+ const logTime = (ms) => {
171
+ const d = new Date(ms);
172
+ const pad = (n, k = 2) => String(n).padStart(k, "0");
173
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
174
+ `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
175
+ };
176
+ export const agentLogLines = (agent, width) => agent.recent.flatMap((line, entry) => {
177
+ const prefix = `${logTime(line.time)}: `;
178
+ const level = `[${line.level.toUpperCase()}]: `;
179
+ const tone = line.level !== "info" ? "signal" : "accent";
180
+ const task = sanitize(line.task);
181
+ const hold = "\uE000".repeat((task !== "" ? task.length + 5 : 0) + level.length - 1);
182
+ const textW = Math.max(10, width - prefix.length);
183
+ const msg = sanitize(line.msg);
184
+ const rows = hold.length < textW ? clampLines(hold + " " + msg, textW, Infinity) :
185
+ [hold, ...clampLines(msg, textW, Infinity)];
186
+ return rows.map((row, k) => ({
187
+ prefix: k === 0 ? prefix : " ".repeat(prefix.length),
188
+ level: k === 0 ? level : "",
189
+ task: k === 0 ? task : "",
190
+ text: k === 0 ? row.slice(hold.length + 1) : row,
191
+ tone,
192
+ entry
193
+ }));
194
+ });
195
+ /* the identity of a processing log line of an agent (for anchoring the scroll position
196
+ of its log to it, as the oldest lines are dropped once the recent lines are complete) */
197
+ export const agentLogKey = (line) => `${line.time}\u0000${line.level}\u0000${line.task}\u0000${line.msg}`;
198
+ /* the tab labels of the read dialog: the task content, followed by its attachments */
199
+ export const dialogTabs = (parts) => parts !== undefined && parts !== null && !(parts instanceof Error) ?
200
+ attachmentTabs(parts.atts, parts.keys.get("Modified") ?? "") : attachmentTabs([]);
201
+ /* the first visible tab of the tab bar, starting at a previous first
202
+ tab, but moved so that the selected tab fits into the width */
203
+ export const tabFirst = (tabs, first, tab, width) => {
204
+ let f = Math.min(first, tab);
205
+ const span = (from) => tabs.slice(from, tab + 1).reduce((n, label) => n + label.length + 3, -1);
206
+ while (f < tab && span(f) > width)
207
+ f++;
208
+ return f;
209
+ };
210
+ /* the layout of the tab bar within its inner width: the visible tabs
211
+ after the first column and separated by a space (with their column
212
+ offsets), a left scroll arrow in the first column while scrolled, a right
213
+ scroll arrow in the last column where further tabs exist, and a too
214
+ wide single tab cut */
215
+ export const tabLayout = (tabs, first, tab, innerW) => {
216
+ const f = tabFirst(tabs, first, tab, innerW - 2);
217
+ const lead = 1;
218
+ const width = innerW - 2;
219
+ const items = [];
220
+ let used = 0;
221
+ let last = f;
222
+ for (; last < tabs.length; last++) {
223
+ const gap = last > f ? 1 : 0;
224
+ let text = ` ${tabs[last]} `;
225
+ if (used + gap + text.length > width) {
226
+ if (last > f)
227
+ break;
228
+ text = text.slice(0, Math.max(0, width - 1)) + "…";
229
+ }
230
+ items.push({ index: last, x: lead + used + gap, text });
231
+ used += gap + text.length;
232
+ }
233
+ return { lead, width, used, items, less: f > 0, more: last < tabs.length };
234
+ };
235
+ /* the number of non-content rows of the read dialog (borders, two
236
+ header lines, the tab bar, four separators, and the footer) */
237
+ export const DIALOG_CHROME = 10;
238
+ /* the column offset of the " X " close button of the read dialog from its right edge */
239
+ export const DIALOG_CLOSE = 5;
240
+ /* the maximum width of the read dialog */
241
+ export const DIALOG_WIDTH = 110;
242
+ /* the segments of the dependencies row of the read dialog within its inner width:
243
+ predecessors on the left, successors on the right (padded in between to the
244
+ full width, or cut at the end if too long), with each id rendered inverse
245
+ behind "≡", with one extra space on each side, and carrying the referenced task id */
246
+ export const refSegs = (pred, succ, tint, innerW) => {
247
+ const refs = (ids) => ids.length === 0 ?
248
+ [{ text: "—", color: palette.dim, inverse: false }] :
249
+ ids.flatMap((ref, i) => [
250
+ ...(i > 0 ? [{ text: " ", color: palette.dim, inverse: false }] : []),
251
+ { text: ` ≡ ${ref} `, color: tint(ref), inverse: true, ref }
252
+ ]);
253
+ const fill = { text: "", color: palette.dim, inverse: false };
254
+ const segs = [
255
+ { text: " predecessors: ", color: palette.dim, inverse: false },
256
+ ...refs(pred),
257
+ fill,
258
+ { text: "successors: ", color: palette.dim, inverse: false },
259
+ ...refs(succ),
260
+ { text: " ", color: palette.dim, inverse: false }
261
+ ];
262
+ const used = segs.reduce((n, seg) => n + seg.text.length, 0);
263
+ fill.text = " ".repeat(Math.max(1, innerW - used));
264
+ let room = innerW;
265
+ for (const seg of segs) {
266
+ if (seg.text.length > room)
267
+ seg.text = room > 0 ? seg.text.slice(0, room - 1) + "…" : "";
268
+ room -= seg.text.length;
269
+ }
270
+ if (room > 0)
271
+ segs[segs.length - 1].text += " ".repeat(room);
272
+ return segs;
273
+ };
274
+ /* the geometry of the confirmation of a task deletion: a small box, centered on
275
+ the screen, and its inverse " delete " and " cancel " buttons, centered in its
276
+ third inner row (as screen positions, for the mouse hit-testing) */
277
+ export const CONFIRM_BUTTON = 8;
278
+ export const confirmBox = (columns, rows) => {
279
+ const width = Math.min(columns - 2, 60);
280
+ const left = Math.floor((columns - width) / 2);
281
+ const top = Math.floor((rows - 7) / 2);
282
+ const pad = Math.max(0, Math.floor((width - 2 - (2 * CONFIRM_BUTTON + 1)) / 2));
283
+ return { width, left, top, pad, row: top + 3, deleteX: left + 1 + pad, cancelX: left + 2 + pad + CONFIRM_BUTTON };
284
+ };
285
+ /* render the confirmation of a task deletion, with every inner cell
286
+ written (with spaces) to hide the content underneath */
287
+ export const renderConfirm = (id, yes, columns, rows) => {
288
+ const { width, left, top, pad } = confirmBox(columns, rows);
289
+ const innerW = width - 2;
290
+ const blank = (key) => h(Text, { key }, " ".repeat(innerW));
291
+ /* the centered question, with the task id rendered inverse with one extra space on each side */
292
+ const tid = ` ${sanitize(id)} `.slice(0, Math.max(0, innerW - 13));
293
+ const qpad = Math.max(0, Math.floor((innerW - 13 - tid.length) / 2));
294
+ const hint = "←/→/⇥: select · ⏎: press · y: delete · ESC: cancel".slice(0, innerW);
295
+ const keys = (" ".repeat(Math.floor((innerW - hint.length) / 2)) + hint).padEnd(innerW);
296
+ return h(Box, { key: "confirm", top, left, width, height: 7, ...cx("popup", "border-signal") }, h(Text, cx("normal", "bold"), " ".repeat(qpad) + "Delete task ", h(Text, cx("inverse"), tid), "?".padEnd(Math.max(0, innerW - qpad - 12 - tid.length))), blank("blank-above"), h(Text, {}, " ".repeat(pad), h(Text, cx("button", yes && "button-active"), " delete "), " ", h(Text, cx("button", !yes && "button-active"), " cancel "), " ".repeat(Math.max(0, innerW - pad - 2 * CONFIRM_BUTTON - 1))), blank("blank-below"), h(Text, cx("dim"), keys));
297
+ };
298
+ /* the first of the at most five visible suggested request names of the drop-down of the
299
+ prompt popup, scrolled to show the selected one */
300
+ export const DROP_ROWS = 5;
301
+ export const dropFirst = (choices, name) => {
302
+ const n = Math.min(DROP_ROWS, choices.length);
303
+ return Math.max(0, Math.min(choices.length - n, choices.indexOf(name) - n + 1));
304
+ };
305
+ /* the geometry of the prompt popup of an edit request (or, with <request>, of a request by
306
+ prompt): a box, centered on the screen and covering about 80% of the terminal width (like
307
+ in the web board), its input boxes (behind the equally wide labels "request name:" and
308
+ "request prompt:" of a request by prompt, for left-aligning them, resp. behind a margin
309
+ only, where the request name box spans 30% of the width of the prompt box), and its
310
+ inverse request and cancel buttons, centered in the row below the prompt (as screen
311
+ positions, for the mouse hit-testing) */
312
+ export const promptBox = (columns, rows, request) => {
313
+ const width = Math.min(columns - 2, Math.max(82, Math.floor(columns * 0.8)));
314
+ const innerW = width - 2;
315
+ const viewH = 5;
316
+ const height = viewH + 9 + (request ? 3 : 0);
317
+ const top = Math.floor((rows - height) / 2);
318
+ const left = Math.floor((columns - width) / 2);
319
+ const labelW = 17;
320
+ const pre = request ? labelW : 1;
321
+ const boxW = Math.max(6, innerW - pre - 1);
322
+ const nameW = Math.max(12, Math.round(boxW * 0.3));
323
+ const okText = request ? " request " : " request edit ";
324
+ const noText = " cancel ";
325
+ const pad = Math.max(0, Math.floor((innerW - okText.length - 1 - noText.length) / 2));
326
+ const promptY = top + 3 + (request ? 3 : 0);
327
+ return {
328
+ width, innerW, viewH, height, top, left, labelW, pre, boxW, nameW, okText, noText, pad,
329
+ nameY: top + 4, dropY: top + 5, promptY, buttonY: promptY + viewH + 2,
330
+ boxX: left + 1 + pre, okX: left + 1 + pad, noX: left + 2 + pad + okText.length
331
+ };
332
+ };
333
+ /* whether the prompt of the prompt popup is ignored: of a request by prompt
334
+ whose request name is only processed by agents ignoring their prompts */
335
+ export const promptIgnored = (prompt) => prompt.request !== null && prompt.request.ignored.includes(prompt.request.name);
336
+ /* the focus of the prompt popup after the request name: the prompt,
337
+ resp. the request button if the prompt is ignored */
338
+ export const promptNext = (prompt) => promptIgnored(prompt) ? "request" : "prompt";
339
+ /* render the prompt popup of an edit request (or, with a request, of a request by prompt of
340
+ any name, with its request name in the frame of an input box above the prompt, and with
341
+ its suggested request names <choices> in a drop-down below it while focused) of a task,
342
+ in the style of the confirmation of a task deletion, with the prompt (with an inverse
343
+ cursor cell at the cursor position, resp. behind its end) broken at its newlines and
344
+ wrapped onto the five lines of the frame of an input box (like in the web board, in
345
+ accent color, resp. in signal color while focused), scrolled to show the cursor, with
346
+ the request and cancel buttons below it (in accent color, resp. in signal color while
347
+ focused), and with
348
+ every inner cell written (with spaces) to hide the content underneath */
349
+ export const renderPrompt = (prompt, columns, rows, choices = []) => {
350
+ const { id, text, pos, request } = prompt;
351
+ const box = promptBox(columns, rows, request !== null);
352
+ const { width, innerW, viewH, height, top, left, labelW, pre, boxW, nameW } = box;
353
+ const blank = (key) => h(Text, { key }, " ".repeat(innerW));
354
+ /* the centered question, with the task id rendered inverse with one extra space on each side */
355
+ const lead = request !== null ? "Request agent for task " : "Edit task ";
356
+ const fixed = lead.length + 11;
357
+ const tid = ` ${sanitize(id)} `.slice(0, Math.max(0, innerW - fixed));
358
+ const qpad = Math.max(0, Math.floor((innerW - fixed - tid.length) / 2));
359
+ const hint = (request !== null ?
360
+ "⇥: switch field · ↑/↓: pick name · ⇧⏎/⌥⏎/^J: new line · ⏎: request · ESC: cancel" :
361
+ "⇥: switch field · ⇧⏎/⌥⏎/^J: new line · ⏎: request edit · ESC: cancel").slice(0, innerW);
362
+ const keys = (" ".repeat(Math.floor((innerW - hint.length) / 2)) + hint).padEnd(innerW);
363
+ /* the cells of the prompt, sanitized per character (with newlines ending their line and
364
+ other control characters shown as spaces), so that the cursor position stays aligned
365
+ with the characters */
366
+ const cells = [];
367
+ const chars = [...text];
368
+ chars.forEach((c, k) => {
369
+ const s = [...(/^\p{Cc}$/u.test(c) && c !== "\t" ? " " : sanitize(c))];
370
+ if (s.length === 0 && k === pos)
371
+ s.push(" ");
372
+ s.forEach((ch, j) => cells.push({ ch, cur: k === pos && j === 0, nl: c === "\n" }));
373
+ });
374
+ if (pos >= chars.length)
375
+ cells.push({ ch: " ", cur: true, nl: false });
376
+ /* the prompt lines, broken after the newlines and cut into chunks of the inner width (minus
377
+ the margins and the frame of the input box), scrolled to show the line of the cursor */
378
+ const lineW = boxW - 4;
379
+ const lines = [[]];
380
+ for (const cell of cells) {
381
+ if (lines[lines.length - 1].length >= lineW)
382
+ lines.push([]);
383
+ lines[lines.length - 1].push(cell);
384
+ if (cell.nl)
385
+ lines.push([]);
386
+ }
387
+ const curL = lines.findIndex((line) => line.some((cell) => cell.cur));
388
+ const first = Math.max(0, Math.min(lines.length - viewH, curL - viewH + 1));
389
+ const shown = Array.from({ length: viewH }, (_, k) => lines[first + k] ?? []);
390
+ /* the request name of a request by prompt in the frame of an input box behind its label
391
+ (in signal color while focused, also its text, with an inverse cursor cell behind its
392
+ end, where the prompt then shows no cursor), with a drop-down marker where request names
393
+ are suggested */
394
+ const indent = " ".repeat(pre);
395
+ const fieldW = nameW - 5;
396
+ const rest = " ".repeat(Math.max(0, innerW - pre - nameW));
397
+ const named = request !== null ? (() => {
398
+ const tone = prompt.focus === "name" ? "signal" : "accent";
399
+ const name = sanitize(request.name).slice(0, fieldW - 1);
400
+ return [
401
+ h(Text, { key: "request-top", ...cx(tone) }, `${indent}╭${"─".repeat(nameW - 2)}╮${rest}`),
402
+ h(Text, { key: "request" }, h(Text, cx("normal"), " request name:".padEnd(labelW)), h(Text, cx(tone), "│ "), h(Text, cx(tone, "bold"), name), h(Text, cx(tone, prompt.focus === "name" && "inverse"), " "), h(Text, {}, " ".repeat(fieldW - 1 - name.length)), h(Text, cx(tone), `${request.choices.length > 0 ? "▾" : " "} │`), rest),
403
+ h(Text, { key: "request-bottom", ...cx(tone) }, `${indent}╰${"─".repeat(nameW - 2)}╯${rest}`)
404
+ ];
405
+ })() : [];
406
+ /* the drop-down of the suggested request names while the request name is focused, attached
407
+ below its input box (overlaying the popup, in signal color), with at most five suggested
408
+ request names (scrolled to show the selected one, rendered inverse, and with markers
409
+ where scrolled) */
410
+ const dropH = Math.min(DROP_ROWS, choices.length);
411
+ const drop = request !== null && prompt.focus === "name" && dropH > 0 ? (() => {
412
+ const sel = choices.indexOf(request.name);
413
+ const first = dropFirst(choices, request.name);
414
+ const more = (flag, mark) => flag ? mark : "─";
415
+ const place = { top: box.dropY, left: box.boxX, width: nameW, height: dropH + 2 };
416
+ return h(Box, { key: "prompt-choices", position: "absolute", flexDirection: "column", ...place }, h(Text, cx("signal"), `├${"─".repeat(nameW - 4)}${more(first > 0, "▴")}─┤`), ...choices.slice(first, first + dropH).map((choice, k) => h(Text, { key: k }, h(Text, cx("signal"), "│"), h(Text, cx("signal", first + k === sel && "inverse", first + k === sel && "bold"), ` ${sanitize(choice).slice(0, nameW - 4).padEnd(nameW - 4)} `), h(Text, cx("signal"), "│"))), h(Text, cx("signal"), `╰${"─".repeat(nameW - 4)}${more(first + dropH < choices.length, "▾")}─╯`));
417
+ })() : null;
418
+ /* the prompt in the frame of an input box (in signal color while focused, also its text,
419
+ resp. dimmed with a notice instead of its text if ignored), behind its label "request
420
+ prompt:" for a request by prompt */
421
+ const ignored = promptIgnored(prompt);
422
+ const notice = "(ignored by the agents of this request)".slice(0, lineW);
423
+ const focus = prompt.focus === "prompt" && !ignored;
424
+ const tone = focus ? "signal" : ignored ? "dim" : "accent";
425
+ const popup = h(Box, { key: "prompt", top, left, width, height, ...cx("popup", "border-dim") }, h(Text, cx("normal", "bold"), " ".repeat(qpad) + lead, h(Text, cx("inverse"), tid), " by prompt:".padEnd(Math.max(0, innerW - qpad - lead.length - tid.length))), blank("blank-above"), ...named, h(Text, { key: "input-top", ...cx(tone) }, `${indent}╭${"─".repeat(boxW - 2)}╮ `), ...shown.map((line, k) => {
426
+ const at = focus ? line.findIndex((cell) => cell.cur) : -1;
427
+ const ch = ignored ? [...(k === 0 ? notice : "")] : line.map((cell) => cell.ch);
428
+ return h(Text, { key: k }, h(Text, cx("normal"), request !== null && k === 0 ? " request prompt:".padEnd(labelW) : indent), h(Text, cx(tone), "│ "), h(Text, cx(tone), ...(at < 0 ? [ch.join("").padEnd(lineW)] : [
429
+ ch.slice(0, at).join(""),
430
+ h(Text, cx("inverse"), ch[at]),
431
+ ch.slice(at + 1).join("").padEnd(Math.max(0, lineW - 1 - at))
432
+ ])), h(Text, cx(tone), " │ "));
433
+ }), h(Text, { key: "input-bottom", ...cx(tone) }, `${indent}╰${"─".repeat(boxW - 2)}╯ `), h(Text, { key: "buttons" }, " ".repeat(box.pad), h(Text, cx("button", prompt.focus === "request" ? "button-active" : "accent"), box.okText), " ", h(Text, cx("button", prompt.focus === "cancel" ? "button-active" : "accent"), box.noText), " ".repeat(Math.max(0, innerW - box.pad - box.okText.length - 1 - box.noText.length))), blank("blank-buttons"), h(Text, cx("dim"), keys));
434
+ return drop !== null ? [popup, drop] : popup;
435
+ };
436
+ /* render the busy popup of a slow task store operation: a small box, centered on the
437
+ screen, with a spinner, the operation, and its elapsed time, above an indeterminate
438
+ progress bar with a bouncing block, with every inner cell written (with spaces) */
439
+ export const renderBusy = (label, since, tick, columns, rows) => {
440
+ const width = Math.min(columns - 2, 50);
441
+ const innerW = width - 2;
442
+ const blank = (key) => h(Text, { key }, " ".repeat(innerW));
443
+ /* the centered operation, with the task id rendered inverse with one extra space on each side */
444
+ const head = `${spinnerFrames[Math.floor(tick / 2) % spinnerFrames.length]} ${sanitize(label.text)}${label.id !== undefined ? " " : ""}`.slice(0, innerW);
445
+ const tid = label.id !== undefined ? ` ${sanitize(label.id)} `.slice(0, innerW - head.length) : "";
446
+ const tail = (label.suffix !== undefined ? ` ${sanitize(label.suffix)}` : "").slice(0, innerW - head.length - tid.length);
447
+ const time = ` (${((Date.now() - since) / 1000).toFixed(1)}s)`.slice(0, innerW - head.length - tid.length - tail.length);
448
+ const used = head.length + tid.length + tail.length + time.length;
449
+ const tpad = Math.floor((innerW - used) / 2);
450
+ /* the progress bar: a block swiping back and forth (eased, one sweep per 1.6s at a
451
+ 50ms tick) in accent color inside a rounded box in accent color, positioned in
452
+ eighth cells via the left eighth blocks, which at the leading edge of the block
453
+ are drawn inverse, as there are no right eighth blocks */
454
+ const barW = Math.max(1, innerW - 4);
455
+ const from = Math.round((barW - Math.min(8, barW)) * 8 * (1 - Math.cos(Math.PI * (tick % 64) / 32)) / 2);
456
+ const to = from + Math.min(8, barW) * 8;
457
+ const segs = [];
458
+ for (let i = 0; i < barW; i++) {
459
+ const lo = 8 * i;
460
+ const seg = to <= lo || from >= lo + 8 ? { text: " ", inverse: false } :
461
+ from <= lo && to >= lo + 8 ? { text: "█", inverse: false } :
462
+ from > lo ? { text: eighths[from - lo], inverse: true } :
463
+ { text: eighths[to - lo], inverse: false };
464
+ const last = segs[segs.length - 1];
465
+ if (last !== undefined && (seg.text === " " || seg.text === "█") && last.text.endsWith(seg.text) && !last.inverse)
466
+ last.text += seg.text;
467
+ else
468
+ segs.push(seg);
469
+ }
470
+ const side = h(Text, {}, " \n \n ");
471
+ return h(Box, { key: "busy", top: Math.floor((rows - 8) / 2), left: Math.floor((columns - width) / 2), width, height: 8, ...cx("popup", "border-dim") },
472
+ /* the parts as siblings (not nested), as a nested text cannot undo the bold and color of its parent */
473
+ h(Box, { flexDirection: "row" }, h(Text, cx("accent", "bold"), " ".repeat(tpad) + head), h(Text, cx("accent", "bold", "inverse"), tid), h(Text, cx("accent", "bold"), tail), h(Text, cx("normal"), time.padEnd(innerW - tpad - used + time.length))), blank("blank-above"), h(Box, { flexDirection: "row" }, side, h(Box, { width: innerW - 2, height: 3, ...cx("frame", "border-accent") }, h(Text, cx("accent"), ...segs.map((seg, k) => h(Text, { key: k, ...cx(seg.inverse && "inverse") }, seg.text)))), side), blank("blank-below"));
474
+ };
475
+ /* the geometry of the transfer popup: centered on the screen, with its entries
476
+ (starting in its third inner row) scrolled so that the selected one stays visible */
477
+ export const transferBox = (count, idx, columns, rows) => {
478
+ const width = Math.min(columns - 2, 60);
479
+ const viewH = Math.max(1, Math.min(count, rows - 8));
480
+ const first = Math.max(0, Math.min(idx - Math.floor(viewH / 2), count - viewH));
481
+ const height = viewH + 6;
482
+ const top = Math.floor((rows - height) / 2);
483
+ return { width, height, viewH, first, top, left: Math.floor((columns - width) / 2), row: top + 3 };
484
+ };
485
+ /* render the transfer popup of a task, in the style of the confirmation of a task
486
+ deletion, with the selected state marked, the current state labeled, and the
487
+ unselectable states dimmed */
488
+ export const renderTransfer = (id, from, entries, at, columns, rows) => {
489
+ const idx = entries.findIndex((e) => e.status === at);
490
+ const box = transferBox(entries.length, idx, columns, rows);
491
+ const innerW = box.width - 2;
492
+ const blank = (key) => h(Text, { key }, " ".repeat(innerW));
493
+ /* the centered question, with the task id rendered inverse with one extra space on each side */
494
+ const tid = ` ${sanitize(id)} `.slice(0, Math.max(0, innerW - 18));
495
+ const qpad = Math.max(0, Math.floor((innerW - 18 - tid.length) / 2));
496
+ const hint = "↑/↓: select · ⏎: transition · ESC: cancel".slice(0, innerW);
497
+ const keys = (" ".repeat(Math.floor((innerW - hint.length) / 2)) + hint).padEnd(innerW);
498
+ return h(Box, { key: "transfer", top: box.top, left: box.left, width: box.width, height: box.height, ...cx("popup", "border-dim") }, h(Text, cx("normal", "bold"), " ".repeat(qpad) + "Transfer task ", h(Text, cx("inverse"), tid), " to:".padEnd(Math.max(0, innerW - qpad - 14 - tid.length))), blank("blank-above"), ...entries.slice(box.first, box.first + box.viewH).map((e, k) => {
499
+ /* the group in normal and the state in bold, cut to the inner width */
500
+ const on = box.first + k === idx;
501
+ const head = ` ${on ? "▶" : " "} ${e.group} ▷ `.slice(0, innerW);
502
+ const state = e.status.slice(0, innerW - head.length);
503
+ const tail = (e.status === from ? " (current)" : "").slice(0, innerW - head.length - state.length);
504
+ return h(Text, { key: k, ...cx("normal", !e.ok && "dim", !e.ok && "dimmed", on && "signal") }, head, h(Text, cx("bold"), state), tail.padEnd(innerW - head.length - state.length));
505
+ }), blank("blank-below"), h(Text, cx("dim"), keys));
506
+ };
507
+ /* the geometry of the assign popup: centered on the screen, with the input box of the
508
+ assignee (its text in the second inner row of it) spanning the inner width (minus a
509
+ margin), and its drop-down of at most five suggested assignees attached below it */
510
+ export const assignBox = (columns, rows) => {
511
+ const width = Math.min(columns - 2, 60);
512
+ const innerW = width - 2;
513
+ const height = DROP_ROWS + 10;
514
+ const top = Math.floor((rows - height) / 2);
515
+ const left = Math.floor((columns - width) / 2);
516
+ return { width, innerW, height, top, left, nameW: innerW - 2, nameX: left + 2, nameY: top + 4, dropY: top + 5 };
517
+ };
518
+ /* render the assign popup of a task, in the style of the request name of the prompt popup:
519
+ the assignee in the frame of an input box (in signal color, with an inverse cursor cell
520
+ behind its end, a drop-down marker, and a marker for an <unknown> assignee), with the
521
+ suggested assignees <choices> (with the icons of their kind) in a drop-down below it
522
+ (scrolled to show the selected one, rendered inverse, and with markers where scrolled),
523
+ and with every inner cell written (with spaces) to hide the content underneath */
524
+ export const renderAssign = (assign, choices, unknown, columns, rows) => {
525
+ const box = assignBox(columns, rows);
526
+ const { width, innerW, height, top, left, nameW } = box;
527
+ const blank = (key) => h(Text, { key }, " ".repeat(innerW));
528
+ /* the centered question, with the task id rendered inverse with one extra space on each side */
529
+ const tid = ` ${sanitize(assign.id)} `.slice(0, Math.max(0, innerW - 16));
530
+ const qpad = Math.max(0, Math.floor((innerW - 16 - tid.length) / 2));
531
+ const hint = "↑/↓/Click: pick assignee · ⏎: assign · ESC: cancel".slice(0, innerW);
532
+ const keys = (" ".repeat(Math.floor((innerW - hint.length) / 2)) + hint).padEnd(innerW);
533
+ /* the assignee (its end shown if too long) and the marker of an unknown assignee */
534
+ const mark = unknown ? "(unknown) " : "";
535
+ const textW = Math.max(1, nameW - 6 - mark.length);
536
+ const name = [...sanitize(assign.name)].slice(-textW).join("");
537
+ /* the suggested assignees, with the selected one rendered inverse */
538
+ const names = choices.map((choice) => choice.name);
539
+ const dropH = Math.min(DROP_ROWS, choices.length);
540
+ const sel = names.indexOf(assign.name);
541
+ const first = dropFirst(names, assign.name);
542
+ const more = (flag, sign) => flag ? sign : "─";
543
+ const icon = (choice) => choice.kind === "user" ? "☺" : choice.kind === "agent" ? "⚉" : " ";
544
+ const drop = dropH === 0 ? [h(Text, { key: "assign-bottom", ...cx("signal") }, ` ╰${"─".repeat(nameW - 2)}╯ `)] : [
545
+ h(Text, { key: "assign-sep", ...cx("signal") }, ` ├${"─".repeat(nameW - 4)}${more(first > 0, "▴")}─┤ `),
546
+ ...choices.slice(first, first + dropH).map((choice, k) => h(Text, { key: `assign-${k}` }, h(Text, cx("signal"), " │"), h(Text, cx("signal", first + k === sel && "inverse", first + k === sel && "bold"), ` ${icon(choice)} ${sanitize(choice.name).slice(0, nameW - 6).padEnd(nameW - 6)} `), h(Text, cx("signal"), "│ "))),
547
+ h(Text, { key: "assign-bottom", ...cx("signal") }, ` ╰${"─".repeat(nameW - 4)}${more(first + dropH < choices.length, "▾")}─╯ `)
548
+ ];
549
+ return h(Box, { key: "assign", top, left, width, height, ...cx("popup", "border-dim") }, h(Text, cx("normal", "bold"), " ".repeat(qpad) + "Assign task ", h(Text, cx("inverse"), tid), " to:".padEnd(Math.max(0, innerW - qpad - 12 - tid.length))), blank("blank-above"), h(Text, { key: "assign-top", ...cx("signal") }, ` ╭${"─".repeat(nameW - 2)}╮ `), h(Text, { key: "assign-name" }, h(Text, cx("signal"), " │ "), h(Text, cx("signal", "bold"), name), h(Text, cx("signal", "inverse"), " "), " ".repeat(Math.max(0, textW - name.length)), h(Text, cx("signal", "bold"), mark), h(Text, cx("signal"), `${names.length > 0 ? "▾" : " "} │ `)), ...drop, ...Array.from({ length: DROP_ROWS - dropH + (dropH === 0 ? 1 : 0) }, (_, k) => blank(`blank-drop-${k}`)), blank("blank-below"), h(Text, cx("dim"), keys));
550
+ };
551
+ /* render the read dialog: full height, horizontally centered, with a
552
+ vertical scroll bar in its right border */
553
+ export const renderDialog = ({ card, group, id, pred, succ, tint, tabs, tab, first, scroll, lines, columns, rows, notice }) => {
554
+ const width = Math.min(columns - 2, DIALOG_WIDTH);
555
+ const left = Math.floor((columns - width) / 2);
556
+ const bodyW = width - 5;
557
+ const viewH = rows - DIALOG_CHROME;
558
+ const maxS = Math.max(0, lines.length - viewH);
559
+ const s = Math.min(scroll, maxS);
560
+ const thumbH = Math.max(1, Math.round(viewH * Math.min(1, viewH / Math.max(1, lines.length))));
561
+ const thumbY = maxS === 0 ? 0 : Math.round((viewH - thumbH) * s / maxS);
562
+ /* without a background fill, every inner cell has to be written
563
+ explicitly (with spaces) to hide the dimmed board underneath */
564
+ const innerW = width - 2;
565
+ /* the header width, without its outer spaces and the " X " close button */
566
+ const titleW = Math.max(0, innerW - 2 - 4);
567
+ /* the header: task group (if any, and not for an epic), task id (behind "≡",
568
+ and of an epic also behind "♛"), and title on the left (with the title cut as needed), lane
569
+ group and status on the right (dropped if even the task group and id would not fit otherwise) */
570
+ const title = card !== undefined ? cardLabel(card).replace(/^(\S+) ▶ /, "$1 ") : id;
571
+ const tgroup = card !== undefined ? groupLabel(card) : "";
572
+ const epic = tgroup !== "" && tgroup === id;
573
+ const prefix = tgroup !== "" && !epic ? `♛ ${tgroup} ▶ ` : "";
574
+ const groupW = prefix.length + (epic ? 4 : 2);
575
+ let right = card !== undefined ? `${group !== undefined ? `${group} ▷ ` : ""}${card.status}` : "";
576
+ if (id.length + 3 + groupW + right.length > titleW)
577
+ right = "";
578
+ else if (right !== "")
579
+ right = " " + right;
580
+ const lane = right !== "" && card !== undefined ? card.status : "";
581
+ /* the id is rendered inverse with one extra space on each side,
582
+ preceded by the task group between "♛" and "▶" in bold */
583
+ const leftW = Math.max(0, titleW - right.length - 2 - groupW);
584
+ const head = title.length > leftW ? title.slice(0, Math.max(0, leftW - 1)) + "…" : title.padEnd(leftW);
585
+ const pos = `lines ${s + 1}–${Math.min(lines.length, s + viewH)} of ${lines.length} `;
586
+ /* the dependencies: predecessors on the left, successors on the right */
587
+ const segs = refSegs(pred, succ, tint, innerW);
588
+ /* the tab bar: all tabs inverse, the selected one in signal color, the others in accent color */
589
+ const lay = tabLayout(tabs, first, tab, innerW);
590
+ const tabBar = [
591
+ { text: lay.less ? "◁" : " ", color: palette.dim, bold: false, inverse: false },
592
+ ...lay.items.flatMap((item, k) => [
593
+ ...(k > 0 ? [{ text: " ", color: undefined, bold: false, inverse: false }] : []),
594
+ { text: item.text, color: item.index === tab ? palette.signal : palette.accent, bold: item.index === tab, inverse: true }
595
+ ]),
596
+ { text: " ".repeat(Math.max(0, lay.width - lay.used)) + (lay.more ? "▷" : " "), color: palette.dim, bold: false, inverse: false }
597
+ ];
598
+ /* the horizontal separator, extended over the side borders to join them with T-glyphs */
599
+ const separator = (key) => h(Box, { key, marginLeft: -1, width, flexShrink: 0 }, h(Text, cx("dim"), "├" + "─".repeat(innerW) + "┤"));
600
+ /* the key hints, or instead the status notice of an edit, truncated to the free width */
601
+ const free = Math.max(0, innerW - pos.length - 1);
602
+ const keys = (notice !== null ? " " + sanitize(notice) :
603
+ " ←/→/⇤/⇥: switch tab · ↑/↓/⇈/⇊: scroll · e: edit · E: edit by prompt · R: request by prompt · T: transition · A: assign · D: delete · M: toggle mouse · ⏎/ESC: close").slice(0, free);
604
+ return h(Box, { key: "dialog", top: 0, left, width, height: rows, ...cx("popup", "border-dim") }, h(Text, {}, " ", ...(prefix !== "" ? [h(Text, { ...cx("bold"), color: tint(id) }, prefix)] : []), h(Text, { ...cx("badge"), color: tint(id) }, ` ${epic ? "♛ " : ""}≡ ${head.slice(0, id.length)} `), h(Text, { color: tint(id) }, head.slice(id.length)), h(Text, cx("dim"), right.slice(0, right.length - lane.length)), h(Text, cx("dim", "bold"), lane), " ", h(Text, cx("dim", "inverse"), " X "), " "), separator("sep-title"), h(Text, {}, ...segs.map((seg, k) => h(Text, { key: k, color: seg.color, inverse: seg.inverse }, seg.text))), separator("sep-head"), h(Text, { wrap: "truncate" }, ...tabBar.map((seg, k) => h(Text, { key: k, color: seg.color, bold: seg.bold, inverse: seg.inverse }, seg.text))), separator("sep-tabs"), ...Array.from({ length: viewH }, (_, i) => {
605
+ const line = lines[s + i];
606
+ const bar = lines.length <= viewH ? " " : (i >= thumbY && i < thumbY + thumbH ? "█" : "░");
607
+ /* split the padded line into runs of equal inline style (cut to the body width,
608
+ as a wider line would wrap and push the rows of the dialog out of place) */
609
+ const text = " " + (line?.text ?? "").slice(0, bodyW).padEnd(bodyW);
610
+ const mask = [0, ...(line?.mask ?? [])];
611
+ const runs = [];
612
+ for (let k = 0; k < text.length; k++) {
613
+ const style = mask[k] ?? 0;
614
+ if (runs.length > 0 && runs[runs.length - 1].style === style)
615
+ runs[runs.length - 1].text += text[k];
616
+ else
617
+ runs.push({ text: text[k], style });
618
+ }
619
+ return h(Box, { key: i }, ...runs.map((run, k) => h(Text, {
620
+ key: k, wrap: "truncate",
621
+ color: (run.style & (CODE | ACCENT)) !== 0 ? palette.accent : (run.style & SIGNAL) !== 0 ? palette.signal :
622
+ (run.style & DIM) !== 0 ? palette.dim : line?.color,
623
+ bold: (line?.bold ?? false) || (run.style & BOLD) !== 0, italic: (run.style & ITALIC) !== 0
624
+ }, run.text)), h(Text, cx("signal"), ` ${bar}`));
625
+ }), separator("sep-foot"), h(Box, {}, h(Text, cx("hint", notice !== null && "signal"), keys), h(Text, cx("hint"), pos.padStart(Math.max(0, innerW - keys.length)))));
626
+ };