@rse/ase 1.0.7 → 2.0.1

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 +357 -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} +8 -1
  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 +269 -81
  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 +231 -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 +81 -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,1008 @@
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 fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import React from "react";
10
+ import { execa } from "execa";
11
+ import { useApp, useWindowSize, measureElement } from "ink";
12
+ import { Task } from "./ase-task.js";
13
+ import { userId } from "./ase-service-otel.js";
14
+ import { buildBoard, watchTasks, BoardState, byCreation, newTaskText, createTask, saveTask, TaskConflict, reachableStates, toneOf, watchAgents, busyAgents, agentsDigest, requestNames, requestPrompt, assignable, dataLoad, viewOrder } from "./ase-task-board-core.js";
15
+ import * as TaskFormat from "./ase-task-format.js";
16
+ import { editRefused, editPrompt, requestEdit, requestsOf } from "./ase-task-request.js";
17
+ import { filterBoard, dropStandalone } from "./ase-task-board-filter.js";
18
+ import { statsOf, statsRanges, effortScale } from "./ase-task-board-stats.js";
19
+ import { layoutGraph } from "./ase-task-board-graph.js";
20
+ import { flowOf, layoutFlow, flowState } from "./ase-task-board-flow.js";
21
+ import { fitGroups, agentLogWidth, agentOpLines, agentOpHeight, AGENT_INFO_H } from "./ase-task-board-tui-view.js";
22
+ import { dialogTabs, planLines, attachmentLines, agentLogLines, agentLogKey, DIALOG_CHROME, DIALOG_WIDTH } from "./ase-task-board-tui-popup.js";
23
+ import { pulseFrames } from "./ase-task-board-tui-style.js";
24
+ /* the suggested request names of a request by prompt, matching its request name as last
25
+ typed (all of them for an empty or an exactly suggested request name, to offer the
26
+ alternatives, and else those containing it) */
27
+ export const requestChoices = (request) => {
28
+ const typed = request.typed.toLowerCase();
29
+ return typed === "" || request.choices.includes(typed) ? request.choices :
30
+ request.choices.filter((choice) => choice.toLowerCase().includes(typed));
31
+ };
32
+ /* the suggested assignees of the assign popup (with "none" first, then the users, then the
33
+ agents, each sorted, where the users win on name clashes), and those matching the assignee
34
+ as last typed (all of them for an empty or an exactly suggested assignee, to offer the
35
+ alternatives, and else those containing it) */
36
+ export const assignOptions = (users, agents) => [
37
+ { name: "none", kind: "none" },
38
+ ...[...new Set(users)].sort().map((name) => ({ name, kind: "user" })),
39
+ ...[...new Set(agents)].filter((name) => !users.includes(name)).sort().map((name) => ({ name, kind: "agent" }))
40
+ ];
41
+ export const assignChoices = (assign) => {
42
+ const typed = assign.typed.toLowerCase();
43
+ return typed === "" || assign.choices.some((choice) => choice.name === assign.typed) ? assign.choices :
44
+ assign.choices.filter((choice) => choice.name.toLowerCase().includes(typed));
45
+ };
46
+ /* whether the assignee of the assign popup is unknown (with restricted assignees only) */
47
+ export const assignUnknown = (assign) => {
48
+ const name = assign.name.trim();
49
+ return assign.strict && name !== "" && name !== "me" && name !== assign.self
50
+ && !assign.choices.some((choice) => choice.name === name);
51
+ };
52
+ /* the navigable items of a group: one per card, or one per lane
53
+ which is minimized, empty, or within a collapsed phase */
54
+ export const groupItems = (board, g, surface) => {
55
+ const out = [];
56
+ const coll = surface.collapsed.includes(board.groups[g].title);
57
+ board.groups[g].lanes.forEach((lane, l) => {
58
+ const cards = board.lanes.get(lane.status) ?? [];
59
+ if (coll || surface.minimized.includes(lane.status) || cards.length === 0)
60
+ out.push({ g, l, id: "" });
61
+ else
62
+ cards.forEach((c) => out.push({ g, l, id: c.id }));
63
+ });
64
+ return out;
65
+ };
66
+ /* re-locate a selection on a (possibly changed) board: a selected card
67
+ is followed into its current lane, otherwise the lane is kept */
68
+ export const relocate = (board, sel, surface) => {
69
+ if (sel.id !== "") {
70
+ const card = board.cards.get(sel.id);
71
+ if (card !== undefined)
72
+ for (let g = 0; g < board.groups.length; g++) {
73
+ const l = board.groups[g].lanes.findIndex((lane) => lane.status === card.status);
74
+ if (l >= 0)
75
+ return surface.minimized.includes(card.status) || surface.collapsed.includes(board.groups[g].title) ?
76
+ { g, l, id: "" } : { g, l, id: card.id };
77
+ }
78
+ }
79
+ const g = Math.min(sel.g, board.groups.length - 1);
80
+ const items = groupItems(board, g, surface);
81
+ return items.find((it) => it.l === sel.l) ?? items[0];
82
+ };
83
+ export const transferEntries = (board, cycle, card) => {
84
+ const reachable = new Set(reachableStates(board, cycle, card));
85
+ return board.groups.flatMap((g) => g.lanes.map((l) => ({
86
+ group: g.title, status: l.status,
87
+ ok: l.status === card.status || reachable.has(l.status)
88
+ })));
89
+ };
90
+ /* the duration (in ms) a task store operation has to last before the busy popup shows it */
91
+ export const BUSY_DELAY = 800;
92
+ /* the duration (in ms) the heart of an agent card stays filled after a received heartbeat */
93
+ const BEAT_SHOWN_MS = 1000;
94
+ /* switch the mouse click reporting of the terminal (xterm, SGR encoding) on or off */
95
+ export const mouseReporting = (on) => {
96
+ process.stdout.write(on ? "\x1b[?1000h\x1b[?1006h" : "\x1b[?1006l\x1b[?1000l");
97
+ };
98
+ /* track the task store operations: once the oldest of the pending operations
99
+ lasts longer than BUSY_DELAY, the modal busy popup shows it (with an animation tick) */
100
+ const useBusy = () => {
101
+ const pending = React.useRef(new Map());
102
+ const seq = React.useRef(0);
103
+ const [busy, setBusy] = React.useState(null);
104
+ const [busyTick, setBusyTick] = React.useState(0);
105
+ const track = (label, op) => {
106
+ const n = ++seq.current;
107
+ pending.current.set(n, label);
108
+ const timer = setTimeout(() => {
109
+ setBusy((b) => b ?? { label: pending.current.values().next().value ?? label, since: Date.now() - BUSY_DELAY });
110
+ }, BUSY_DELAY);
111
+ return op.finally(() => {
112
+ clearTimeout(timer);
113
+ pending.current.delete(n);
114
+ const next = pending.current.values().next();
115
+ setBusy((b) => b === null || next.done === true ? null : { ...b, label: next.value });
116
+ });
117
+ };
118
+ React.useEffect(() => {
119
+ if (busy === null)
120
+ return;
121
+ const timer = setInterval(() => {
122
+ setBusyTick((n) => n + 1);
123
+ }, 50);
124
+ return () => {
125
+ clearInterval(timer);
126
+ };
127
+ }, [busy]);
128
+ return { busy, busyTick, track };
129
+ };
130
+ /* the rendered boxes for the mouse hit-testing: the card, lane, and group boxes of the
131
+ lane view, the agent cards of the agents view, the time range buttons of the statistics
132
+ view, the view value, filter field, and clear button of the header, and the graph viewport content */
133
+ const useHitBoxes = () => {
134
+ const cardBoxes = React.useRef(new Map());
135
+ const laneBoxes = React.useRef(new Map());
136
+ const groupBoxes = React.useRef(new Map());
137
+ const headBoxes = React.useRef(new Map());
138
+ const agentBoxes = React.useRef(new Map());
139
+ const rangeBoxes = React.useRef(new Map());
140
+ const graphView = React.useRef(null);
141
+ const register = (map, key) => (el) => {
142
+ if (el !== null)
143
+ map.set(key, el);
144
+ else
145
+ map.delete(key);
146
+ };
147
+ const cardRef = (id) => register(cardBoxes.current, id);
148
+ const laneRef = (g, l) => register(laneBoxes.current, `${g}:${l}`);
149
+ const groupRef = (g) => register(groupBoxes.current, String(g));
150
+ const headRef = (key) => register(headBoxes.current, key);
151
+ const agentRef = (id) => register(agentBoxes.current, id);
152
+ const rangeRef = (range) => register(rangeBoxes.current, range);
153
+ return { cardBoxes, laneBoxes, groupBoxes, headBoxes, agentBoxes, rangeBoxes, graphView, cardRef, laneRef, groupRef, headRef, agentRef, rangeRef };
154
+ };
155
+ /* find the registered box under a (0-based) mouse position */
156
+ const boxAt = (map, mx, my) => {
157
+ for (const [key, el] of map) {
158
+ const m = measureElement(el);
159
+ if (mx >= m.x && mx < m.x + m.width && my >= m.y && my < m.y + m.height)
160
+ return key;
161
+ }
162
+ return undefined;
163
+ };
164
+ /* the state of the agents view: the agents of the master list, the selected one (followed by its id,
165
+ else, until explicitly selected again, the one at the index of the previously selected one),
166
+ the lines of its operation pane, and the lines of its recent processing log in the detail view (within its frame), scrolled
167
+ to the bottom unless explicitly scrolled up, where the scroll position is anchored to the
168
+ processing log line at its top (as the oldest lines are dropped once the recent lines are
169
+ complete), falling back to the top once this line was dropped, too */
170
+ const useAgentsView = (agents, columns, boardH) => {
171
+ const [agentSel, setAgentSel] = React.useState("");
172
+ const [logScroll, setLogScroll] = React.useState(null);
173
+ const agentList = agents?.agents ?? [];
174
+ const agentHit = agentList.findIndex((a) => a.id === agentSel);
175
+ const agentLast = React.useRef(0);
176
+ const agentIdx = agentHit >= 0 ? agentHit : Math.max(0, Math.min(agentLast.current, agentList.length - 1));
177
+ const agent = agentList.at(agentIdx);
178
+ const agentOp = React.useMemo(() => agentOpLines(agent, columns), [agent, columns]);
179
+ const logLines = React.useMemo(() => agent !== undefined ? agentLogLines(agent, agentLogWidth(columns)) : [], [agent, columns]);
180
+ const logH = Math.max(1, boardH - agentOpHeight(agentOp) - AGENT_INFO_H - 2);
181
+ const logMax = Math.max(0, logLines.length - logH);
182
+ let logFirst = logMax;
183
+ if (logScroll !== null && agentHit >= 0 && agent !== undefined) {
184
+ const entry = agent.recent.findIndex((line) => agentLogKey(line) === logScroll.key);
185
+ logFirst = entry < 0 ? 0 :
186
+ Math.max(0, Math.min(logMax, logLines.findIndex((line) => line.entry === entry) + logScroll.offset));
187
+ }
188
+ /* remember the index of the selected agent, and once it vanished (or initially), explicitly
189
+ select the agent at its previous index (resp. the first one), following its log at the bottom */
190
+ React.useEffect(() => {
191
+ if (agentHit >= 0)
192
+ agentLast.current = agentHit;
193
+ else if (agentList.length > 0) {
194
+ setAgentSel(agentList[agentIdx].id);
195
+ setLogScroll(null);
196
+ }
197
+ }, [agentHit, agentIdx, agentList]);
198
+ /* select another agent (relative to the selected one), following its log at the bottom */
199
+ const selectAgent = (delta) => {
200
+ if (agentList.length === 0)
201
+ return;
202
+ setAgentSel(agentList[Math.max(0, Math.min(agentList.length - 1, agentIdx + delta))].id);
203
+ setLogScroll(null);
204
+ };
205
+ /* scroll the log of the detail view (by lines, or to its top or bottom),
206
+ anchored to the processing log line of its top row, and following it
207
+ again at the bottom once scrolled down to it */
208
+ const scrollLog = (delta) => {
209
+ const next = delta === "top" ? 0 : delta === "bottom" ? logMax : Math.max(0, Math.min(logMax, logFirst + delta));
210
+ const line = logLines[next];
211
+ if (next >= logMax || line === undefined || agent === undefined) {
212
+ setLogScroll(null);
213
+ return;
214
+ }
215
+ const start = logLines.findIndex((l) => l.entry === line.entry);
216
+ setLogScroll({ key: agentLogKey(agent.recent[line.entry]), offset: next - start });
217
+ };
218
+ /* select an agent by its id (e.g. from the agent graph), following its log at the bottom */
219
+ const focusAgent = (id) => {
220
+ setAgentSel(id);
221
+ setLogScroll(null);
222
+ };
223
+ return { agentList, agentIdx, agent, agentOp, logLines, logH, logFirst, logMax, selectAgent, scrollLog, focusAgent };
224
+ };
225
+ /* the state of the terminal board: all state, the derived values, and the actions */
226
+ export const useBoardState = (log, initial, relay) => {
227
+ const { exit, suspendTerminal } = useApp();
228
+ const { columns, rows } = useWindowSize();
229
+ const [all, setBoard] = React.useState(initial);
230
+ const [surface, setSurface] = React.useState(() => BoardState.load().tui);
231
+ const [view, setView] = React.useState(() => surface.view);
232
+ const [filter, setFilter] = React.useState("");
233
+ const [typing, setTyping] = React.useState(false);
234
+ /* the shown board: all tasks reduced onto the ones matching the filter
235
+ query (plus, in the graph view, their direct dependencies as context,
236
+ and optionally without the standalone tasks) */
237
+ const board = React.useMemo(() => {
238
+ const found = filterBoard(all, filter, view === "graph");
239
+ return view === "graph" && !surface.standalone ? dropStandalone(found) : found;
240
+ }, [all, filter, view, surface.standalone]);
241
+ const [sel, setSel] = React.useState(() => relocate(board, { g: 0, l: 0, id: "" }, surface));
242
+ const [dialog, setDialog] = React.useState(null);
243
+ const [plan, setPlan] = React.useState(null);
244
+ const [files, setFiles] = React.useState(new Map());
245
+ const [first, setFirst] = React.useState(0);
246
+ const [scroll, setScroll] = React.useState({ x: 0, y: 0 });
247
+ const [layout, setLayout] = React.useState(null);
248
+ const [flowLayout, setFlowLayout] = React.useState(null);
249
+ const [flowSel, setFlowSel] = React.useState("");
250
+ const followed = React.useRef("");
251
+ const [notice, setNotice] = React.useState(null);
252
+ const [carry, setCarry] = React.useState(null);
253
+ const [cycle, setCycle] = React.useState(null);
254
+ const [confirm, setConfirm] = React.useState(null);
255
+ const [transfer, setTransfer] = React.useState(null);
256
+ const [prompt, setPrompt] = React.useState(null);
257
+ const [assign, setAssign] = React.useState(null);
258
+ const [store, setStore] = React.useState(null);
259
+ const [relayed, setRelayed] = React.useState(undefined);
260
+ const [usage, setUsage] = React.useState(null);
261
+ const [range, setRange] = React.useState("total");
262
+ const [grow, setGrow] = React.useState(false);
263
+ const editing = React.useRef(false);
264
+ const drafts = React.useRef(new Map());
265
+ /* the drafts of the prompts of cancelled (or failed to be sent) edit requests, per task */
266
+ const promptDrafts = React.useRef(new Map());
267
+ /* the drafts of the cancelled (or failed to be sent) requests by prompt, per task */
268
+ const requestDrafts = React.useRef(new Map());
269
+ /* the busy popup of slow task store operations */
270
+ const { busy, busyTick, track } = useBusy();
271
+ /* the rendered boxes for the mouse hit-testing */
272
+ const { cardBoxes, laneBoxes, groupBoxes, headBoxes, agentBoxes, rangeBoxes, graphView, cardRef, laneRef, groupRef, headRef, agentRef, rangeRef } = useHitBoxes();
273
+ /* report mouse clicks while the board runs and mouse support is enabled
274
+ (disabling it gives the regular text selection of the terminal back) */
275
+ const [mouse, setMouse] = React.useState(true);
276
+ const mouseOn = React.useRef(true);
277
+ React.useEffect(() => {
278
+ mouseOn.current = mouse;
279
+ mouseReporting(mouse);
280
+ return () => {
281
+ mouseReporting(false);
282
+ };
283
+ }, [mouse]);
284
+ /* the last left click (cell and time), for detecting double clicks */
285
+ const lastClick = React.useRef(null);
286
+ /* fetch the task lifecycle model (for checking the moves of tasks)
287
+ whenever the board and hence possibly its lifecycle mode changes */
288
+ React.useEffect(() => {
289
+ let live = true;
290
+ Task.lifecycle(log).then((lifecycle) => {
291
+ if (live)
292
+ setCycle(lifecycle);
293
+ }).catch((err) => {
294
+ log.write("warning", `board: loading lifecycle failed: ${err instanceof Error ? err.message : String(err)}`);
295
+ });
296
+ return () => {
297
+ live = false;
298
+ };
299
+ }, [log, all]);
300
+ /* persist the shown view for the next start of the terminal board */
301
+ React.useEffect(() => {
302
+ BoardState.setView("tui", view).catch((err) => {
303
+ log.write("warning", `board: persisting view failed: ${err instanceof Error ? err.message : String(err)}`);
304
+ });
305
+ }, [log, view]);
306
+ /* auto-clear a status notice after 5 seconds */
307
+ React.useEffect(() => {
308
+ if (notice === null)
309
+ return;
310
+ const timer = setTimeout(() => {
311
+ setNotice(null);
312
+ }, 5 * 1000);
313
+ return () => {
314
+ clearTimeout(timer);
315
+ };
316
+ }, [notice]);
317
+ /* drop a carried task if it vanished or changed its status in the meantime */
318
+ React.useEffect(() => {
319
+ if (carry !== null && all.cards.get(carry.id)?.status !== carry.from) {
320
+ setCarry(null);
321
+ setNotice(`moving task "${carry.id}" cancelled: task changed in the meantime`);
322
+ }
323
+ }, [all, carry]);
324
+ /* close the transfer popup if its task vanished in the meantime */
325
+ React.useEffect(() => {
326
+ if (transfer !== null && !all.cards.has(transfer.id)) {
327
+ setTransfer(null);
328
+ setNotice(`transferring task "${transfer.id}" cancelled: task vanished in the meantime`);
329
+ }
330
+ }, [all, transfer]);
331
+ /* close the assign popup if its task vanished in the meantime */
332
+ React.useEffect(() => {
333
+ if (assign !== null && !all.cards.has(assign.id)) {
334
+ setAssign(null);
335
+ setNotice(`assigning task "${assign.id}" cancelled: task vanished in the meantime`);
336
+ }
337
+ }, [all, assign]);
338
+ /* close the prompt popup if its task vanished in the meantime */
339
+ React.useEffect(() => {
340
+ if (prompt !== null && !all.cards.has(prompt.id)) {
341
+ setPrompt(null);
342
+ setNotice(`editing task "${prompt.id}" by prompt cancelled: task vanished in the meantime`);
343
+ }
344
+ }, [all, prompt]);
345
+ /* remember the box of every graph node for the spatial navigation and the scrolling */
346
+ const places = React.useMemo(() => {
347
+ const map = new Map();
348
+ if (layout !== null)
349
+ for (const n of layout.graph.nodes.values())
350
+ map.set(n.id, { r: n.y + Math.floor(n.h / 2), c: n.x + Math.floor(n.w / 2), top: n.y, bottom: n.y + n.h - 1, bl: n.x, br: n.x + n.w - 1 });
351
+ return map;
352
+ }, [layout]);
353
+ /* follow changes of the task storage and of the lifecycle mode,
354
+ and the kind and connection state of the task store
355
+ (in the background, i.e. without the busy popup) */
356
+ React.useEffect(() => {
357
+ const refresh = async () => {
358
+ setBoard(await buildBoard(log));
359
+ };
360
+ const stop = watchTasks(log, refresh, setStore);
361
+ return () => {
362
+ stop().catch(() => { });
363
+ };
364
+ }, [log]);
365
+ /* follow the agents passed by the agent relay (if any, else null, and undefined until
366
+ known), where the agents view and the agent graph fall back to the lanes view without
367
+ an agent relay, and where, outside of them, a frame of the agent relay is taken over only
368
+ if it visibly changes the agents there (and not on a mere heartbeat or on new
369
+ processing log lines), in order to not needlessly re-render the board, while the
370
+ latest frame is taken over as soon as the agents view or the agent graph is entered */
371
+ const viewRef = React.useRef(view);
372
+ viewRef.current = view;
373
+ const latest = React.useRef(undefined);
374
+ React.useEffect(() => {
375
+ return watchAgents(log, relay, (next) => {
376
+ latest.current = next;
377
+ setRelayed((prev) => viewRef.current === "agents" || viewRef.current === "flow"
378
+ || agentsDigest(prev) !== agentsDigest(next) ? next : prev);
379
+ });
380
+ }, [log, relay]);
381
+ React.useEffect(() => {
382
+ if ((view === "agents" || view === "flow") && latest.current !== undefined)
383
+ setRelayed(latest.current);
384
+ }, [view]);
385
+ const agents = relayed ?? null;
386
+ const working = React.useMemo(() => busyAgents(agents), [agents]);
387
+ React.useEffect(() => {
388
+ if ((view === "agents" || view === "flow") && relayed === null)
389
+ setView("lanes");
390
+ }, [view, relayed]);
391
+ /* cycle (forward or backward) through the views: lanes, graph, stats, and (with an agent relay) agents and flow */
392
+ const cycleView = (back = false) => {
393
+ const views = viewOrder(agents !== null);
394
+ setView(views[(views.indexOf(view) + (back ? views.length - 1 : 1)) % views.length]);
395
+ };
396
+ /* the agent graph of the agents (with the lifecycle states placed as in the lanes), laid out
397
+ only whenever its structure changes (and not on a mere change of the contents of its cards),
398
+ with the selected agent card kept valid */
399
+ const flow = React.useMemo(() => flowOf(agents?.agents ?? [], all.groups), [agents, all]);
400
+ const flowKey = JSON.stringify([[...flow.cards.keys()], flow.states, flow.edges]);
401
+ React.useEffect(() => {
402
+ if (view !== "flow" || flowLayout?.key === flowKey)
403
+ return;
404
+ let live = true;
405
+ layoutFlow(flow, "cell").then((g) => {
406
+ if (live)
407
+ setFlowLayout({ key: flowKey, graph: g });
408
+ }).catch((err) => {
409
+ log.write("warning", `board: agent graph layout failed: ${err instanceof Error ? err.message : String(err)}`);
410
+ });
411
+ return () => {
412
+ live = false;
413
+ };
414
+ }, [log, view, flowKey]);
415
+ React.useEffect(() => {
416
+ if (!flow.cards.has(flowSel) && !flow.states.some((s) => flowState(s) === flowSel) && flow.cards.size > 0)
417
+ setFlowSel(flow.cards.keys().next().value);
418
+ }, [flow, flowSel]);
419
+ const flowPlaces = React.useMemo(() => {
420
+ const map = new Map();
421
+ if (flowLayout !== null)
422
+ for (const n of flowLayout.graph.nodes.values())
423
+ map.set(n.id, { r: n.y + Math.floor(n.h / 2), c: n.x + Math.floor(n.w / 2), top: n.y, bottom: n.y + n.h - 1, bl: n.x, br: n.x + n.w - 1 });
424
+ return map;
425
+ }, [flowLayout]);
426
+ /* fetch the AI usage of all tasks whenever the statistics view is shown or the board
427
+ changes, but only while the statistics view is shown (as each fetching loads all
428
+ plans), and once known in the background, i.e. without the busy popup */
429
+ React.useEffect(() => {
430
+ if (view !== "stats")
431
+ return;
432
+ let live = true;
433
+ const load = Task.listUsage(log);
434
+ const run = usage === null ? track({ text: "loading usage of tasks" }, load) : load;
435
+ run.then((next) => {
436
+ if (live)
437
+ setUsage(next);
438
+ }).catch((err) => {
439
+ log.write("warning", `board: loading usage failed: ${err instanceof Error ? err.message : String(err)}`);
440
+ });
441
+ return () => {
442
+ live = false;
443
+ };
444
+ }, [log, all, view]);
445
+ /* the statistics of the shown tasks within the time range (while the statistics view is shown),
446
+ with their effort on the configured scale (re-read on each change of the board) */
447
+ const stats = React.useMemo(() => view === "stats" ? statsOf(board, all, usage ?? new Map(), range, effortScale(log)) : null, [log, board, all, usage, view, range]);
448
+ /* step the time range of the statistics to the previous or next one (without wrapping around) */
449
+ const stepRange = (delta) => {
450
+ const k = statsRanges.indexOf(range) + delta;
451
+ if (k >= 0 && k < statsRanges.length)
452
+ setRange(statsRanges[k]);
453
+ };
454
+ /* fetch the plan (with its attachments) of the read dialog whenever it is opened or the board changes
455
+ (the latter in the background, i.e. without the busy popup), together with loading the
456
+ syntax highlighter of YAML or JSON attachments (on the first opening only) */
457
+ const dialogId = dialog?.id;
458
+ const dialogTab = dialog?.tab ?? 0;
459
+ React.useEffect(() => {
460
+ if (dialogId === undefined)
461
+ return;
462
+ let live = true;
463
+ const load = Promise.all([Task.parts(log, dialogId), Task.attachments(log, dialogId), dataLoad()]);
464
+ const run = plan?.id === dialogId ? load : track({ text: "loading task", id: dialogId }, load);
465
+ run.then(([parts, atts]) => {
466
+ if (live)
467
+ setPlan({ id: dialogId, parts: parts === null ? null : { ...parts, atts } });
468
+ }).catch((err) => {
469
+ log.write("warning", `board: loading plan failed: ${err instanceof Error ? err.message : String(err)}`);
470
+ if (live)
471
+ setPlan({ id: dialogId, parts: err instanceof Error ? err : new Error(String(err)) });
472
+ });
473
+ return () => {
474
+ live = false;
475
+ };
476
+ }, [log, all, dialogId]);
477
+ /* fetch the file content of the attachment of the selected tab, whenever the tab is selected or the plan changes
478
+ (the latter in the background, i.e. without the busy popup) */
479
+ React.useEffect(() => {
480
+ if (plan === null || plan.id !== dialogId)
481
+ return;
482
+ const { id, parts } = plan;
483
+ if (parts === undefined || parts === null || parts instanceof Error)
484
+ return;
485
+ const tab = Math.min(dialogTab, parts.atts.length);
486
+ if (tab === 0 || parts.atts[tab - 1].file === undefined)
487
+ return;
488
+ const key = `${id}:${tab}`;
489
+ let live = true;
490
+ const load = Task.attachmentContent(log, id, tab - 1);
491
+ const run = files.has(key) ? load : track({ text: "loading attachment of task", id }, load);
492
+ run.then((content) => content?.content ?? new Error("no such attachment content")).catch((err) => err instanceof Error ? err : new Error(String(err))).then((content) => {
493
+ if (live)
494
+ setFiles(new Map([[key, content]]));
495
+ });
496
+ return () => {
497
+ live = false;
498
+ };
499
+ }, [log, plan, dialogId, dialogTab]);
500
+ /* lay out the dependency graph whenever it is shown, the board changes,
501
+ the showing of task titles is toggled, or the window width changes
502
+ (as every task box gets a fixed quarter of the graph viewport width) */
503
+ const graphTitles = surface.titles;
504
+ const graphNodeW = Math.max(12, Math.floor((columns - 6) / 4));
505
+ React.useEffect(() => {
506
+ if (view !== "graph")
507
+ return;
508
+ let live = true;
509
+ layoutGraph(board, "cell", graphTitles, graphNodeW).then((g) => {
510
+ if (live)
511
+ setLayout({ board, graph: g, titles: graphTitles });
512
+ }).catch((err) => {
513
+ log.write("warning", `board: graph layout failed: ${err instanceof Error ? err.message : String(err)}`);
514
+ });
515
+ return () => {
516
+ live = false;
517
+ };
518
+ }, [log, board, view, graphTitles, graphNodeW]);
519
+ /* toggle a minimized lane or a collapsed phase (resp. multiple at once) of the TUI surface */
520
+ const toggle = (list, entry) => {
521
+ BoardState.toggle("tui", list, entry).then((state) => {
522
+ setSurface(state.tui);
523
+ }).catch((err) => {
524
+ log.write("warning", `board: toggling ${list} failed: ${err instanceof Error ? err.message : String(err)}`);
525
+ });
526
+ };
527
+ /* run $EDITOR on a text in a temporary file, handing the terminal over
528
+ to the editor, and return the edited text */
529
+ const runEditor = async (name, text) => {
530
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "ase-task-"));
531
+ const file = path.join(dir, `${name}.md`);
532
+ try {
533
+ await fs.promises.writeFile(file, text, "utf8");
534
+ /* run $EDITOR through the shell (to support values with arguments or
535
+ quoting), passing the file via the environment to avoid quoting it */
536
+ const editor = (process.env.EDITOR ?? "").trim() || "vi";
537
+ const ref = process.platform === "win32" ? "\"%ASE_TASK_FILE%\"" : "\"$ASE_TASK_FILE\"";
538
+ await suspendTerminal(async () => {
539
+ mouseReporting(false);
540
+ try {
541
+ await execa(`${editor} ${ref}`, { shell: true, stdio: "inherit", env: { ASE_TASK_FILE: file } });
542
+ }
543
+ finally {
544
+ mouseReporting(mouseOn.current);
545
+ }
546
+ });
547
+ return await fs.promises.readFile(file, "utf8");
548
+ }
549
+ finally {
550
+ await fs.promises.rm(dir, { recursive: true, force: true });
551
+ }
552
+ };
553
+ /* edit a task with $EDITOR; a draft which failed to save is kept and
554
+ offered again on the next edit of the same task */
555
+ const edit = async (id) => {
556
+ const src = await track({ text: "loading task", id }, Task.source(log, id));
557
+ if (src === null) {
558
+ setNotice(`task "${id}" no longer exists`);
559
+ return;
560
+ }
561
+ const text = await runEditor(id, drafts.current.get(id) ?? src.text);
562
+ if (text === src.text) {
563
+ drafts.current.delete(id);
564
+ setNotice(`task "${id}" unchanged`);
565
+ return;
566
+ }
567
+ try {
568
+ /* conditionally save with the entity tag, to refuse overwriting changes
569
+ made meanwhile by others (e.g. an agent or the web board) */
570
+ const { id: next, warning } = await track({ text: "saving task", id }, saveTask(log, id, text, src.tag));
571
+ drafts.current.delete(id);
572
+ if (next !== id) {
573
+ setSel((s) => s.id === id ? { ...s, id: next } : s);
574
+ setDialog((d) => d?.id === id ? { ...d, id: next } : d);
575
+ }
576
+ setNotice((next !== id ? `task "${id}" saved and renamed to "${next}"` : `task "${id}" saved`) +
577
+ (warning !== "" ? ` (${warning})` : ""));
578
+ }
579
+ catch (err) {
580
+ if (err instanceof TaskConflict) {
581
+ if (err.tag === null) {
582
+ drafts.current.delete(id);
583
+ setNotice(`task "${id}" was deleted meanwhile (edit discarded)`);
584
+ }
585
+ else {
586
+ drafts.current.set(id, text);
587
+ setNotice(`task "${id}" was changed meanwhile (press "e" to re-edit and overwrite)`);
588
+ }
589
+ return;
590
+ }
591
+ drafts.current.set(id, text);
592
+ const msg = err instanceof Error ? err.message : String(err);
593
+ setNotice(`saving task "${id}" failed: ${msg} (press "e" to re-edit)`);
594
+ }
595
+ };
596
+ /* create a new task with $EDITOR on its pre-filled text, under the id of
597
+ its "Id:" key; an unchanged pre-filled text creates no task, and a text which
598
+ failed to save is kept as a draft (under the empty id) for the next new task,
599
+ where even an unchanged draft is created again, while an emptied text
600
+ discards the draft (and any new task) */
601
+ const create = async () => {
602
+ const draft = drafts.current.get("");
603
+ const orig = draft ?? await track({ text: "preparing new task" }, Task.lifecycle(log).then((lifecycle) => newTaskText(log, all, lifecycle)));
604
+ const text = await runEditor("new-task", orig);
605
+ if ((draft === undefined && text === orig) || text.trim() === "") {
606
+ drafts.current.delete("");
607
+ setNotice(draft !== undefined ? "new task draft discarded" : "new task discarded");
608
+ return;
609
+ }
610
+ const id = TaskFormat.taskTextId(text);
611
+ try {
612
+ const warning = await track({ text: "creating task", id }, createTask(log, id, text));
613
+ drafts.current.delete("");
614
+ setSel((s) => ({ ...s, id }));
615
+ setNotice(`task "${id}" created` + (warning !== "" ? ` (${warning})` : ""));
616
+ }
617
+ catch (err) {
618
+ drafts.current.set("", text);
619
+ const msg = err instanceof Error ? err.message : String(err);
620
+ setNotice(`creating task failed: ${msg} (press "N" to re-edit, or empty it to discard)`);
621
+ }
622
+ };
623
+ /* delete a task (after its confirmation), closing its read dialog */
624
+ const remove = (id) => {
625
+ if (dialog?.id === id)
626
+ setDialog(null);
627
+ track({ text: "deleting task", id }, Task.delete(log, id)).then((existed) => {
628
+ setNotice(existed ? `task "${id}" deleted` : `task "${id}" no longer exists`);
629
+ }).catch((err) => {
630
+ setNotice(`deleting task "${id}" failed: ${err instanceof Error ? err.message : String(err)}`);
631
+ });
632
+ };
633
+ /* transfer a task (from its popup) to a state, where its current state cancels */
634
+ const transferTo = (id, to) => {
635
+ setTransfer(null);
636
+ if (to === all.cards.get(id)?.status) {
637
+ setNotice(`transferring task "${id}" cancelled`);
638
+ return;
639
+ }
640
+ track({ text: "moving task", id, suffix: `to ${to}` }, Task.setStatus(log, id, to)).then((result) => {
641
+ setNotice(`task "${id}" moved from ${result.from} to ${result.to}`);
642
+ }).catch((err) => {
643
+ setNotice(`moving task "${id}" failed: ${err instanceof Error ? err.message : String(err)}`);
644
+ });
645
+ };
646
+ /* open the assign popup of a task, prefilled with its assignee, suggesting the users of
647
+ "project.users" (else the current user) and the assignable agents present at the agent relay */
648
+ const startAssign = (id) => {
649
+ const card = all.cards.get(id);
650
+ if (card === undefined)
651
+ return;
652
+ let users;
653
+ try {
654
+ users = Task.users(log);
655
+ }
656
+ catch (err) {
657
+ setNotice(`assigning task "${id}" failed: ${err instanceof Error ? err.message : String(err)}`);
658
+ return;
659
+ }
660
+ const self = userId();
661
+ const present = (latest.current ?? agents)?.agents.filter(assignable).map((a) => a.name) ?? [];
662
+ const choices = assignOptions(users.length > 0 ? users : [self], present);
663
+ setAssign({ id, name: card.assignee, typed: "", choices, self, strict: users.length > 0 });
664
+ };
665
+ /* assign a task (from its popup) to an assignee ("me" for the current user, and
666
+ empty or "none" to unassign it), where an unknown assignee is assigned with a warning */
667
+ const assignTo = (id, name) => {
668
+ setAssign(null);
669
+ const to = name.trim() === "me" ? userId() : name.trim();
670
+ const op = Task.checkAssignee(log, to).then(async (warning) => ({ warning, from: await Task.assign(log, id, to === "" || to === "none" ? null : to) }));
671
+ track({ text: "assigning task", id, suffix: `to ${to === "" ? "none" : to}` }, op).then(({ warning, from }) => {
672
+ setNotice(`task "${id}" assigned from "${from || "(nobody)"}" to "${to === "" || to === "none" ? "(nobody)" : to}"` +
673
+ (warning !== "" ? ` -- warning: ${warning}` : ""));
674
+ }).catch((err) => {
675
+ setNotice(`assigning task "${id}" failed: ${err instanceof Error ? err.message : String(err)}`);
676
+ });
677
+ };
678
+ /* drop a carried task onto a lane state, keeping it selected */
679
+ const drop = (id, to) => {
680
+ setCarry(null);
681
+ track({ text: "moving task", id, suffix: `to ${to}` }, Task.setStatus(log, id, to)).then((result) => {
682
+ setSel({ g: sel.g, l: sel.l, id });
683
+ setNotice(`task "${id}" moved from ${result.from} to ${result.to}`);
684
+ }).catch((err) => {
685
+ setNotice(`moving task "${id}" failed: ${err instanceof Error ? err.message : String(err)}`);
686
+ });
687
+ };
688
+ /* edit an existing task, or create a new one (without id) */
689
+ const startEdit = (id) => {
690
+ if (editing.current)
691
+ return;
692
+ editing.current = true;
693
+ setNotice(null);
694
+ const run = id !== null ? edit(id) : create();
695
+ run.catch((err) => {
696
+ setNotice(`${id !== null ? `editing task "${id}"` : "creating task"} failed: ${err instanceof Error ? err.message : String(err)}`);
697
+ }).finally(() => {
698
+ editing.current = false;
699
+ });
700
+ };
701
+ /* the token of the pending loading of the prompt of an edit request, invalidated by every
702
+ later loading and every change of the popups, the read dialog, the selection, or the view
703
+ (so a slowly loaded prompt never opens its popup over another one or for another task) */
704
+ const promptLoad = React.useRef(0);
705
+ React.useEffect(() => {
706
+ promptLoad.current++;
707
+ }, [confirm, transfer, assign, prompt, dialog?.id, sel.id, view]);
708
+ /* open the prompt popup of an edit request of a task (refused for a finished task, and
709
+ while an edit of it is in progress, as a new request would change the task under the
710
+ running edit), prefilled with its draft of a cancelled (or failed to be sent) prompt,
711
+ else with the prompt of its most recent failed edit request (for a retry) */
712
+ const startPrompt = (id) => {
713
+ const card = all.cards.get(id);
714
+ if (card === undefined)
715
+ return;
716
+ if (cycle !== null && editRefused(cycle, card.actual)) {
717
+ setNotice(`editing task "${id}" by prompt refused: task is finished`);
718
+ return;
719
+ }
720
+ if (card.edit === "active") {
721
+ setNotice(`editing task "${id}" by prompt refused: edit in progress`);
722
+ return;
723
+ }
724
+ const draft = promptDrafts.current.get(id);
725
+ if (draft !== undefined) {
726
+ setPrompt({ id, ...draft, focus: "prompt", request: null });
727
+ return;
728
+ }
729
+ const token = ++promptLoad.current;
730
+ track({ text: "loading edit prompt of task", id }, editPrompt(log, id)).then((text) => {
731
+ if (token === promptLoad.current)
732
+ setPrompt({ id, text, pos: [...text].length, focus: "prompt", request: null });
733
+ }).catch((err) => {
734
+ if (token === promptLoad.current)
735
+ setNotice(`editing task "${id}" by prompt failed: ${err instanceof Error ? err.message : String(err)}`);
736
+ });
737
+ };
738
+ /* open the prompt popup of a request by prompt of any name of a task (see "ase task
739
+ request", refused for a finished task), with the request name suggested from the
740
+ request agents announced to the agent relay, prefilled with its draft of a cancelled
741
+ (or failed to be sent) request, where a missing request name is to be entered first */
742
+ const startRequest = (id) => {
743
+ const card = all.cards.get(id);
744
+ if (card === undefined)
745
+ return;
746
+ if (cycle !== null && editRefused(cycle, card.actual)) {
747
+ setNotice(`requesting task "${id}" by prompt refused: task is finished`);
748
+ return;
749
+ }
750
+ const choices = requestNames(agents);
751
+ const ignored = choices.filter((choice) => requestPrompt(agents, choice) === "ignored");
752
+ const draft = requestDrafts.current.get(id);
753
+ const name = draft?.name ?? choices[0] ?? "";
754
+ const focus = name === "" ? "name" : ignored.includes(name) ? "request" : "prompt";
755
+ setPrompt({ id, text: draft?.text ?? "", pos: draft?.pos ?? 0, focus, request: { name, choices, ignored, typed: "" } });
756
+ };
757
+ /* cancel the prompt popup, keeping its prompt as the draft of the task (even if
758
+ empty, so that a cleared prompt is not prefilled again on reopening) */
759
+ const cancelPrompt = () => {
760
+ if (prompt === null)
761
+ return;
762
+ if (prompt.request !== null) {
763
+ requestDrafts.current.set(prompt.id, { name: prompt.request.name, text: prompt.text, pos: prompt.pos });
764
+ setNotice(`requesting task "${prompt.id}" by prompt cancelled`);
765
+ }
766
+ else {
767
+ promptDrafts.current.set(prompt.id, { text: prompt.text, pos: prompt.pos });
768
+ setNotice(`editing task "${prompt.id}" by prompt cancelled`);
769
+ }
770
+ setPrompt(null);
771
+ };
772
+ /* request a task by the prompt of its popup under its request name (processed by a
773
+ request agent of a definition, resp. by "ase agent editor" for "edit"), dropping
774
+ the draft of the task once sent, resp. keeping the request as its draft on failure,
775
+ where the prompt may be empty only for the requests of agents not requiring their prompts,
776
+ and is dropped for the requests of agents ignoring their prompts */
777
+ const submitRequest = (id, text, pos, name) => {
778
+ let requests;
779
+ try {
780
+ requests = requestsOf(name);
781
+ }
782
+ catch (err) {
783
+ setNotice(`requesting task "${id}" by prompt failed: ${err instanceof Error ? err.message : String(err)}`);
784
+ return;
785
+ }
786
+ const use = requestPrompt(agents, name);
787
+ if (text.trim() === "" && use === "required") {
788
+ setNotice(`requesting "${name}" of task "${id}" failed: empty prompt`);
789
+ return;
790
+ }
791
+ setPrompt(null);
792
+ track({ text: `requesting "${name}" of task`, id }, requests.request(log, id, use === "ignored" ? "" : text)).then(() => {
793
+ requestDrafts.current.delete(id);
794
+ setNotice(`"${name}" of task "${id}" requested`);
795
+ }).catch((err) => {
796
+ requestDrafts.current.set(id, { name, text, pos });
797
+ setNotice(`requesting "${name}" of task "${id}" failed: ${err instanceof Error ? err.message : String(err)}`);
798
+ });
799
+ };
800
+ /* request the edit of a task by the prompt of its popup (processed by "ase agent editor"),
801
+ dropping the draft of the task once sent, resp. keeping the prompt as its draft on failure,
802
+ or request a task by the prompt of its popup under its request name */
803
+ const submitPrompt = () => {
804
+ if (prompt === null)
805
+ return;
806
+ const { id, text, pos } = prompt;
807
+ if (prompt.request !== null) {
808
+ submitRequest(id, text, pos, prompt.request.name);
809
+ return;
810
+ }
811
+ if (text.trim() === "") {
812
+ setNotice(`requesting edit of task "${id}" failed: empty edit prompt`);
813
+ return;
814
+ }
815
+ setPrompt(null);
816
+ track({ text: "requesting edit of task", id }, requestEdit(log, id, text)).then(() => {
817
+ promptDrafts.current.delete(id);
818
+ setNotice(`edit of task "${id}" requested`);
819
+ }).catch((err) => {
820
+ promptDrafts.current.set(id, { text, pos });
821
+ setNotice(`requesting edit of task "${id}" failed: ${err instanceof Error ? err.message : String(err)}`);
822
+ });
823
+ };
824
+ /* toggle a flag (task titles, key hints, or standalone tasks) of the TUI surface */
825
+ const toggleFlag = (flag) => {
826
+ BoardState.toggleFlag("tui", flag).then((state) => {
827
+ setSurface(state.tui);
828
+ }).catch((err) => {
829
+ log.write("warning", `board: toggling ${flag} failed: ${err instanceof Error ? err.message : String(err)}`);
830
+ });
831
+ };
832
+ /* keep the selection valid on every board, surface, or view change
833
+ (the graph view shows all nodes, so it ignores minimized lanes and collapsed phases) */
834
+ React.useEffect(() => {
835
+ setSel((s) => relocate(board, s, view === "graph" ? { ...surface, minimized: [], collapsed: [] } : surface));
836
+ }, [board, surface, view]);
837
+ /* the selected lane is grown to the full board only while it is neither
838
+ minimized nor within a collapsed phase (else it shrinks back for good) */
839
+ const growable = board.groups[sel.g] !== undefined
840
+ && !surface.collapsed.includes(board.groups[sel.g].title)
841
+ && !surface.minimized.includes(board.groups[sel.g].lanes[sel.l]?.status ?? "");
842
+ const grown = grow && growable;
843
+ React.useEffect(() => {
844
+ if (grow && !growable)
845
+ setGrow(false);
846
+ }, [grow, growable]);
847
+ const headH = 3;
848
+ const footH = surface.keys ? (view === "agents" || view === "stats" || view === "flow" ? 5 : view === "lanes" ? 7 : 6) : 3;
849
+ const boardH = rows - headH - footH;
850
+ const innerW = columns - 2;
851
+ /* the agents view: its agents, the selected one, and the scrolled processing log of the latter */
852
+ const { agentList, agentIdx, agent, agentOp, logLines, logH, logFirst, logMax, selectAgent, scrollLog, focusAgent } = useAgentsView(agents, columns, boardH);
853
+ /* the tabs of the read dialog, the lines of its selected tab (clamped to
854
+ the existing tabs), and the maximum scroll offset of these lines */
855
+ const dialogW = Math.min(columns - 2, DIALOG_WIDTH);
856
+ const dialogParts = dialog !== null && plan?.id === dialog.id ? plan.parts : undefined;
857
+ const tabLabels = dialogTabs(dialogParts);
858
+ const dialogSel = Math.min(dialogTab, tabLabels.length - 1);
859
+ const dialogAtt = dialogSel > 0 && dialogParts !== undefined && dialogParts !== null && !(dialogParts instanceof Error) ?
860
+ dialogParts.atts[dialogSel - 1] : undefined;
861
+ const dialogLines = dialog === null ? [] : dialogAtt !== undefined ?
862
+ attachmentLines(dialogAtt, files.get(`${dialog.id}:${dialogSel}`), Math.max(1, dialogW - 5)) :
863
+ planLines(dialogParts, dialog.id, Math.max(1, dialogW - 5));
864
+ const dialogMax = Math.max(0, dialogLines.length - (rows - DIALOG_CHROME));
865
+ /* the entries of the transfer popup of its (still existing) task */
866
+ const transferCard = transfer !== null ? all.cards.get(transfer.id) : undefined;
867
+ const transferList = transferCard !== undefined && cycle !== null ? transferEntries(all, cycle, transferCard) : [];
868
+ /* scroll the selected tab of the read dialog */
869
+ const dialogScroll = (delta) => {
870
+ if (dialog === null)
871
+ return;
872
+ const scroll = Math.max(0, Math.min(dialogMax, (dialog.scrolls[dialogSel] ?? 0) + delta));
873
+ setDialog({ ...dialog, scrolls: { ...dialog.scrolls, [dialogSel]: scroll } });
874
+ };
875
+ /* determine the visible groups, following the selection
876
+ (storing the derived state during rendering is idempotent) */
877
+ let fit = fitGroups(board.groups, surface.collapsed, Math.min(first, board.groups.length - 1), innerW);
878
+ while (sel.g < fit.first)
879
+ fit = fitGroups(board.groups, surface.collapsed, fit.first - 1, innerW);
880
+ while (sel.g > fit.last && fit.first < board.groups.length - 1)
881
+ fit = fitGroups(board.groups, surface.collapsed, fit.first + 1, innerW);
882
+ if (fit.first !== first)
883
+ setFirst(fit.first);
884
+ /* scroll the viewport of the graph (resp. the agent graph) so the box of the selected node
885
+ stays visible, with the rows of a surrounding group frame (padding) above and below it,
886
+ where the agent graph follows its selection only once per change of it, as its viewport
887
+ can also be scrolled freely (to its state boxes, which are not selectable) */
888
+ const viewH = boardH - 2;
889
+ const viewW = innerW - 4;
890
+ let { x, y } = scroll;
891
+ const box = view === "graph" ? places.get(sel.id) : view === "flow" ? flowPlaces.get(flowSel) : undefined;
892
+ const height = view === "graph" ? layout?.graph.height : flowLayout?.graph.height;
893
+ const follow = `${view}:${flowSel}`;
894
+ if (box !== undefined && height !== undefined && (view !== "flow" || followed.current !== follow)) {
895
+ const top = Math.max(0, box.top - 2);
896
+ const bottom = Math.min(height, box.bottom + 1);
897
+ if (top < y)
898
+ y = top;
899
+ if (bottom > y + viewH - 1)
900
+ y = bottom - viewH + 1;
901
+ if (box.bl < x)
902
+ x = Math.max(0, box.bl - 2);
903
+ if (box.br > x + viewW - 1)
904
+ x = box.br - viewW + 3;
905
+ if (view === "flow")
906
+ followed.current = follow;
907
+ }
908
+ if (view !== "flow")
909
+ followed.current = "";
910
+ if (x !== scroll.x || y !== scroll.y)
911
+ setScroll({ x, y });
912
+ /* scroll the viewport of the agent graph freely (within its bounds) */
913
+ const scrollFlow = (dx, dy) => {
914
+ const g = flowLayout?.graph;
915
+ if (g === undefined)
916
+ return;
917
+ setScroll((s) => ({
918
+ x: Math.max(0, Math.min(Math.max(0, g.width + 2 - viewW), s.x + dx)),
919
+ y: Math.max(0, Math.min(Math.max(0, g.height + 1 - viewH), s.y + dy))
920
+ }));
921
+ };
922
+ /* the graph navigation order: by level, then by creation time */
923
+ const nodes = [...board.cards.values()].sort((a, b) => board.levels.get(a.id) - board.levels.get(b.id) || byCreation(a, b));
924
+ /* the board is dimmed while a dialog or popup is shown (incl. the busy popup) */
925
+ const dim = dialog !== null || confirm !== null || transfer !== null || assign !== null || prompt !== null || busy !== null;
926
+ /* pulse the tasks of active lanes (or the active nodes of the graph, or the busy agents), but only while
927
+ any of them (or its pulse) is actually visible, in order to not needlessly re-render the board */
928
+ const shown = grown ? [{ ...board.groups[sel.g], lanes: [board.groups[sel.g].lanes[sel.l]] }] : board.groups.slice(fit.first, fit.last + 1);
929
+ const agentsBusy = (view === "agents" && agentList.some((a) => a.state === "busy"))
930
+ || (view === "flow" && flowLayout !== null && [...flowLayout.graph.nodes.values()].some((n) => flow.cards.get(n.id)?.busy === true
931
+ && n.y >= y && n.y < y + viewH && n.x + n.w - 3 >= x && n.x + n.w - 3 < x + viewW));
932
+ const pulsing = !dim && (agentsBusy || (view === "lanes" ?
933
+ shown.some((g) => !surface.collapsed.includes(g.title)
934
+ && g.lanes.some((l) => l.active && !surface.minimized.includes(l.status) && (board.lanes.get(l.status)?.length ?? 0) > 0)) :
935
+ view === "graph" && layout !== null && [...layout.graph.nodes.values()].some((n) => !layout.board.context.has(n.id)
936
+ && toneOf(layout.board, layout.board.cards.get(n.id)) === "active"
937
+ && n.y >= y && n.y < y + viewH && n.x + n.w - 3 >= x && n.x + n.w - 3 < x + viewW)));
938
+ const [pulseTick, setPulseTick] = React.useState(0);
939
+ React.useEffect(() => {
940
+ if (!pulsing)
941
+ return;
942
+ const timer = setInterval(() => {
943
+ setPulseTick((n) => (n + 1) % pulseFrames.length);
944
+ }, 250);
945
+ return () => {
946
+ clearInterval(timer);
947
+ };
948
+ }, [pulsing]);
949
+ const pulse = pulsing ? pulseTick : -1;
950
+ /* tick the wall-clock every second while the agents view is shown, for the live
951
+ durations of its footer (the time in the current state and the age of the heartbeat) */
952
+ const [now, setNow] = React.useState(Date.now());
953
+ React.useEffect(() => {
954
+ if (view !== "agents")
955
+ return;
956
+ setNow(Date.now());
957
+ const timer = setInterval(() => {
958
+ setNow(Date.now());
959
+ }, 1000);
960
+ return () => {
961
+ clearInterval(timer);
962
+ };
963
+ }, [view]);
964
+ /* the local time at which the latest heartbeat of each agent was received (measured
965
+ locally, as the clock of the agent relay may be skewed), for the heart of its card
966
+ (filled for BEAT_SHOWN_MS after a heartbeat, else empty), where the heartbeats are
967
+ recognized by their changed time stamps and a filled heart is re-rendered once it is
968
+ due to be emptied (as the wall-clock tick alone would empty it too early or too late) */
969
+ const beats = React.useRef(new Map());
970
+ const beatsSeen = React.useMemo(() => {
971
+ const at = Date.now();
972
+ const list = agents?.agents ?? [];
973
+ const next = new Map();
974
+ for (const a of list) {
975
+ const prev = beats.current.get(a.id);
976
+ next.set(a.id, prev !== undefined && prev.beat === a.beat ? prev : { beat: a.beat, seen: at });
977
+ }
978
+ beats.current = next;
979
+ return next;
980
+ }, [agents]);
981
+ React.useEffect(() => {
982
+ if (view !== "agents" && view !== "flow")
983
+ return;
984
+ const latest = Math.max(0, ...[...beatsSeen.values()].map((b) => b.seen));
985
+ const due = latest + BEAT_SHOWN_MS - Date.now();
986
+ if (latest === 0 || due <= 0)
987
+ return;
988
+ const timer = setTimeout(() => {
989
+ setNow(Date.now());
990
+ }, due);
991
+ return () => {
992
+ clearTimeout(timer);
993
+ };
994
+ }, [view, beatsSeen]);
995
+ const beatOf = (id) => now - (beatsSeen.get(id)?.seen ?? 0) < BEAT_SHOWN_MS;
996
+ return {
997
+ log, exit, columns, rows, all, board, surface, view, setView, filter, setFilter, typing, setTyping,
998
+ sel, setSel, dialog, setDialog, scroll, layout, notice, setNotice, carry, setCarry, cycle,
999
+ confirm, setConfirm, transfer, setTransfer, assign, setAssign, prompt, setPrompt, mouse, setMouse, lastClick, grown, setGrow,
1000
+ cardBoxes, laneBoxes, groupBoxes, headBoxes, agentBoxes, rangeBoxes, cardRef, laneRef, groupRef, headRef, agentRef, rangeRef, boxAt, graphView,
1001
+ places, graphTitles, boardH, dialogW, tabLabels, dialogSel, dialogLines, dialogScroll,
1002
+ transferCard, transferList, fit, viewH, viewW, x, y, nodes, dim, pulse, now, beatOf, store, busy, busyTick,
1003
+ agents, working, cycleView, agentList, agentIdx, agent, agentOp, logLines, logH, logFirst, logMax, selectAgent, scrollLog, stats, usage, range, setRange, stepRange,
1004
+ flow, flowLayout, flowSel, setFlowSel, flowPlaces, focusAgent, scrollFlow,
1005
+ toggle, toggleFlag, startEdit, startPrompt, startRequest, cancelPrompt, submitPrompt, remove, transferTo,
1006
+ startAssign, assignTo, drop
1007
+ };
1008
+ };