@rse/ase 2.0.1 → 2.0.3

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 (155) hide show
  1. package/dst/ase-agent-definition-core.js +111 -87
  2. package/dst/ase-agent-fleet.js +1 -1
  3. package/dst/ase-agent-runtime-claim.js +5 -3
  4. package/dst/ase-agent-runtime-engine.js +1 -1
  5. package/dst/ase-agent-runtime-phase.js +1 -1
  6. package/dst/ase-agent-runtime-request.js +1 -1
  7. package/dst/ase-agent-runtime.js +3 -5
  8. package/dst/ase-artifact.js +3 -3
  9. package/dst/ase-config-core.js +2 -16
  10. package/dst/ase-config-schema.js +1 -3
  11. package/dst/ase-config.js +4 -1
  12. package/dst/ase-hook.js +14 -19
  13. package/dst/ase-lib-daemon-cli.js +117 -4
  14. package/dst/ase-lib-server-cli.js +10 -10
  15. package/dst/ase-mcp.js +4 -4
  16. package/dst/ase-service-getopt.js +2 -2
  17. package/dst/ase-service-otel.js +4 -4
  18. package/dst/ase-service-skills.js +5 -15
  19. package/dst/ase-service.js +87 -210
  20. package/dst/ase-setup-common.js +1 -2
  21. package/dst/ase-setup-settings.js +6 -9
  22. package/dst/ase-setup.js +2 -3
  23. package/dst/ase-statusline.js +170 -170
  24. package/dst/ase-task-board-core.js +1 -1
  25. package/dst/ase-task-board-flow.js +7 -6
  26. package/dst/ase-task-board-graph.js +5 -4
  27. package/dst/ase-task-board-tui-control.js +51 -45
  28. package/dst/ase-task-board-tui-model.js +195 -181
  29. package/dst/ase-task-board-tui-popup.js +2 -3
  30. package/dst/ase-task-board-tui-view.js +88 -79
  31. package/dst/ase-task-board-web-client.css +1 -1
  32. package/dst/ase-task-board-web-client.js +2 -2
  33. package/dst/ase-task-board-web-server.js +4 -27
  34. package/dst/ase-task-board.js +1 -1
  35. package/dst/ase-task-format.js +1 -1
  36. package/dst/ase-task-store-client.js +17 -5
  37. package/dst/ase-task-store-core.js +5 -7
  38. package/dst/ase-task-store-issues.js +91 -0
  39. package/dst/ase-task-store-plugin-ase.js +7 -3
  40. package/dst/ase-task-store-plugin-gitea.js +11 -97
  41. package/dst/ase-task-store-plugin-github.js +5 -77
  42. package/dst/ase-task-store-plugin-gitlab.js +4 -83
  43. package/dst/ase-task.js +51 -23
  44. package/dst/ase-util-compat.js +1 -1
  45. package/dst/ase-util-diagram.js +16 -42
  46. package/dst/ase.js +5 -5
  47. package/dst/man/ase-agent-adhoc.1 +1 -1
  48. package/dst/man/ase-agent-condenser.1 +1 -1
  49. package/dst/man/ase-agent-editor.1 +1 -1
  50. package/dst/man/ase-agent-estimator.1 +1 -1
  51. package/dst/man/ase-agent-fleet.1 +1 -1
  52. package/dst/man/ase-agent-implementer.1 +1 -1
  53. package/dst/man/ase-agent-integrator.1 +1 -1
  54. package/dst/man/ase-agent-qualifier.1 +1 -1
  55. package/dst/man/ase-agent-rebooter.1 +1 -1
  56. package/dst/man/ase-agent-relay.1 +1 -1
  57. package/dst/man/ase-agent-researcher.1 +1 -1
  58. package/dst/man/ase-agent.1 +1 -1
  59. package/dst/man/ase-artifact-list.1 +1 -1
  60. package/dst/man/ase-artifact-name.1 +1 -1
  61. package/dst/man/ase-artifact.1 +1 -1
  62. package/dst/man/ase-config-delete.1 +1 -1
  63. package/dst/man/ase-config-edit.1 +1 -1
  64. package/dst/man/ase-config-get.1 +1 -1
  65. package/dst/man/ase-config-init.1 +1 -1
  66. package/dst/man/ase-config-list.1 +1 -1
  67. package/dst/man/ase-config-set.1 +1 -1
  68. package/dst/man/ase-config.1 +1 -1
  69. package/dst/man/ase-hook-permission-request.1 +1 -1
  70. package/dst/man/ase-hook-pre-tool-use.1 +1 -1
  71. package/dst/man/ase-hook-session-end.1 +1 -1
  72. package/dst/man/ase-hook-session-persona.1 +1 -1
  73. package/dst/man/ase-hook-session-start.1 +1 -1
  74. package/dst/man/ase-hook-stop.1 +1 -1
  75. package/dst/man/ase-hook-user-prompt-submit.1 +1 -1
  76. package/dst/man/ase-hook.1 +1 -1
  77. package/dst/man/ase-man.1 +1 -1
  78. package/dst/man/ase-mcp.1 +1 -1
  79. package/dst/man/ase-service-log.1 +1 -1
  80. package/dst/man/ase-service-send.1 +1 -1
  81. package/dst/man/ase-service-start.1 +1 -1
  82. package/dst/man/ase-service-status.1 +1 -1
  83. package/dst/man/ase-service-stop.1 +1 -1
  84. package/dst/man/ase-service.1 +1 -1
  85. package/dst/man/ase-setup-disable.1 +1 -1
  86. package/dst/man/ase-setup-enable.1 +1 -1
  87. package/dst/man/ase-setup-install.1 +1 -1
  88. package/dst/man/ase-setup-mcp.1 +1 -1
  89. package/dst/man/ase-setup-status.1 +1 -1
  90. package/dst/man/ase-setup-statusline.1 +1 -1
  91. package/dst/man/ase-setup-telemetry.1 +1 -1
  92. package/dst/man/ase-setup-uninstall.1 +1 -1
  93. package/dst/man/ase-setup-update.1 +1 -1
  94. package/dst/man/ase-setup.1 +1 -1
  95. package/dst/man/ase-spec-export.1 +1 -1
  96. package/dst/man/ase-spec-lint.1 +1 -1
  97. package/dst/man/ase-spec-preview.1 +1 -1
  98. package/dst/man/ase-spec.1 +1 -1
  99. package/dst/man/ase-statusline.1 +1 -1
  100. package/dst/man/ase-task-assign.1 +1 -1
  101. package/dst/man/ase-task-board.1 +3 -3
  102. package/dst/man/ase-task-delete.1 +1 -1
  103. package/dst/man/ase-task-edit.1 +1 -1
  104. package/dst/man/ase-task-idscheme.1 +1 -1
  105. package/dst/man/ase-task-lifecycle.1 +1 -1
  106. package/dst/man/ase-task-list.1 +1 -1
  107. package/dst/man/ase-task-load.1 +1 -1
  108. package/dst/man/ase-task-newid.1 +1 -1
  109. package/dst/man/ase-task-purge.1 +1 -1
  110. package/dst/man/ase-task-rename.1 +1 -1
  111. package/dst/man/ase-task-request.1 +1 -1
  112. package/dst/man/ase-task-save.1 +1 -1
  113. package/dst/man/ase-task-status.1 +1 -1
  114. package/dst/man/ase-task-store.1 +1 -1
  115. package/dst/man/ase-task-view.1 +1 -1
  116. package/dst/man/ase-task.1 +1 -1
  117. package/dst/man/ase-util-compat.1 +1 -1
  118. package/dst/man/ase-util-diagram.1 +1 -1
  119. package/dst/man/ase-util-meta.1 +1 -1
  120. package/dst/man/ase-util-metric.1 +1 -1
  121. package/dst/man/ase-util-mint.1 +1 -1
  122. package/dst/man/ase-util-worktree.1 +1 -1
  123. package/dst/man/ase-util.1 +1 -1
  124. package/dst/man/ase.1 +1 -1
  125. package/package.json +1 -1
  126. package/plugin/.claude-plugin/plugin.json +1 -1
  127. package/plugin/.codex-plugin/plugin.json +1 -1
  128. package/plugin/.github/plugin/plugin.json +1 -1
  129. package/plugin/meta/ase-common-resolve.md +2 -2
  130. package/plugin/meta/ase-skill.md +7 -0
  131. package/plugin/mods/ase-mods.ts +16 -12
  132. package/plugin/skills/ase-arch-analyze/SKILL.md +3 -1
  133. package/plugin/skills/ase-code-analyze/SKILL.md +26 -10
  134. package/plugin/skills/ase-code-analyze/help.md +11 -6
  135. package/plugin/skills/ase-code-edit/SKILL.md +5 -4
  136. package/plugin/skills/ase-code-edit/help.md +1 -1
  137. package/plugin/skills/ase-code-lint/SKILL.md +52 -27
  138. package/plugin/skills/ase-code-lint/help.md +10 -5
  139. package/plugin/skills/ase-help-intent/data.md +60 -30
  140. package/plugin/skills/ase-help-skill/help.md +3 -4
  141. package/plugin/skills/ase-meta-quorum/SKILL.md +9 -0
  142. package/plugin/skills/ase-meta-quorum/help.md +5 -3
  143. package/plugin/skills/ase-meta-workflow/sample.md +5 -3
  144. package/plugin/skills/ase-repo-changelog/SKILL.md +8 -9
  145. package/plugin/skills/ase-repo-changelog/help.md +2 -2
  146. package/plugin/skills/ase-repo-review/SKILL.md +5 -3
  147. package/plugin/skills/ase-repo-review/help.md +3 -1
  148. package/plugin/skills/ase-spec-edit/SKILL.md +2 -1
  149. package/plugin/skills/ase-spec-edit/help.md +2 -1
  150. package/plugin/skills/ase-sync-reconcile/help.md +4 -4
  151. package/plugin/skills/ase-task-dissect/SKILL.md +10 -6
  152. package/plugin/skills/ase-task-grill/help.md +1 -1
  153. package/plugin/skills/ase-task-implement/SKILL.md +58 -1
  154. package/plugin/skills/ase-task-implement/help.md +16 -0
  155. package/plugin/skills/ase-task-list/help.md +2 -2
@@ -161,6 +161,14 @@ const boxAt = (map, mx, my) => {
161
161
  }
162
162
  return undefined;
163
163
  };
164
+ /* the box of every node of a laid out graph, for the spatial navigation and the scrolling */
165
+ const placesOf = (graph) => {
166
+ const map = new Map();
167
+ if (graph !== undefined)
168
+ for (const n of graph.nodes.values())
169
+ 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 });
170
+ return map;
171
+ };
164
172
  /* the state of the agents view: the agents of the master list, the selected one (followed by its id,
165
173
  else, until explicitly selected again, the one at the index of the previously selected one),
166
174
  the lines of its operation pane, and the lines of its recent processing log in the detail view (within its frame), scrolled
@@ -222,6 +230,183 @@ const useAgentsView = (agents, columns, boardH) => {
222
230
  };
223
231
  return { agentList, agentIdx, agent, agentOp, logLines, logH, logFirst, logMax, selectAgent, scrollLog, focusAgent };
224
232
  };
233
+ /* the wall-clock and the heartbeats of the agents of the agents view and the agent graph */
234
+ const useBeats = (agents, view) => {
235
+ /* tick the wall-clock every second while the agents view is shown, for the live
236
+ durations of its footer (the time in the current state and the age of the heartbeat) */
237
+ const [now, setNow] = React.useState(Date.now());
238
+ React.useEffect(() => {
239
+ if (view !== "agents")
240
+ return;
241
+ setNow(Date.now());
242
+ const timer = setInterval(() => {
243
+ setNow(Date.now());
244
+ }, 1000);
245
+ return () => {
246
+ clearInterval(timer);
247
+ };
248
+ }, [view]);
249
+ /* the local time at which the latest heartbeat of each agent was received (measured
250
+ locally, as the clock of the agent relay may be skewed), for the heart of its card
251
+ (filled for BEAT_SHOWN_MS after a heartbeat, else empty), where the heartbeats are
252
+ recognized by their changed time stamps and a filled heart is re-rendered once it is
253
+ due to be emptied (as the wall-clock tick alone would empty it too early or too late) */
254
+ const beats = React.useRef(new Map());
255
+ const beatsSeen = React.useMemo(() => {
256
+ const at = Date.now();
257
+ const list = agents?.agents ?? [];
258
+ const next = new Map();
259
+ for (const a of list) {
260
+ const prev = beats.current.get(a.id);
261
+ next.set(a.id, prev !== undefined && prev.beat === a.beat ? prev : { beat: a.beat, seen: at });
262
+ }
263
+ beats.current = next;
264
+ return next;
265
+ }, [agents]);
266
+ React.useEffect(() => {
267
+ if (view !== "agents" && view !== "flow")
268
+ return;
269
+ const latest = Math.max(0, ...[...beatsSeen.values()].map((b) => b.seen));
270
+ const due = latest + BEAT_SHOWN_MS - Date.now();
271
+ if (latest === 0 || due <= 0)
272
+ return;
273
+ const timer = setTimeout(() => {
274
+ setNow(Date.now());
275
+ }, due);
276
+ return () => {
277
+ clearTimeout(timer);
278
+ };
279
+ }, [view, beatsSeen]);
280
+ const beatOf = (id) => now - (beatsSeen.get(id)?.seen ?? 0) < BEAT_SHOWN_MS;
281
+ return { now, beatOf };
282
+ };
283
+ /* the actions of the prompt popup of the terminal board: opening it for an edit request
284
+ or for a request by prompt of any name, cancelling it, and submitting it, with the
285
+ drafts of the cancelled (or failed to be sent) prompts and requests, per task */
286
+ const usePromptActions = (ctx) => {
287
+ const { log, all, cycle, agents, prompt, setPrompt, setNotice, track, promptLoad } = ctx;
288
+ /* the drafts of the prompts of cancelled (or failed to be sent) edit requests, per task */
289
+ const promptDrafts = React.useRef(new Map());
290
+ /* the drafts of the cancelled (or failed to be sent) requests by prompt, per task */
291
+ const requestDrafts = React.useRef(new Map());
292
+ /* open the prompt popup of an edit request of a task (refused for a finished task, and
293
+ while an edit of it is in progress, as a new request would change the task under the
294
+ running edit), prefilled with its draft of a cancelled (or failed to be sent) prompt,
295
+ else with the prompt of its most recent failed edit request (for a retry) */
296
+ const startPrompt = (id) => {
297
+ const card = all.cards.get(id);
298
+ if (card === undefined)
299
+ return;
300
+ if (cycle !== null && editRefused(cycle, card.actual)) {
301
+ setNotice(`editing task "${id}" by prompt refused: task is finished`);
302
+ return;
303
+ }
304
+ if (card.edit === "active") {
305
+ setNotice(`editing task "${id}" by prompt refused: edit in progress`);
306
+ return;
307
+ }
308
+ const draft = promptDrafts.current.get(id);
309
+ if (draft !== undefined) {
310
+ setPrompt({ id, ...draft, focus: "prompt", request: null });
311
+ return;
312
+ }
313
+ const token = ++promptLoad.current;
314
+ track({ text: "loading edit prompt of task", id }, editPrompt(log, id)).then((text) => {
315
+ if (token === promptLoad.current)
316
+ setPrompt({ id, text, pos: [...text].length, focus: "prompt", request: null });
317
+ }).catch((err) => {
318
+ if (token === promptLoad.current)
319
+ setNotice(`editing task "${id}" by prompt failed: ${err instanceof Error ? err.message : String(err)}`);
320
+ });
321
+ };
322
+ /* open the prompt popup of a request by prompt of any name of a task (see "ase task
323
+ request", refused for a finished task), with the request name suggested from the
324
+ request agents announced to the agent relay, prefilled with its draft of a cancelled
325
+ (or failed to be sent) request, where a missing request name is to be entered first */
326
+ const startRequest = (id) => {
327
+ const card = all.cards.get(id);
328
+ if (card === undefined)
329
+ return;
330
+ if (cycle !== null && editRefused(cycle, card.actual)) {
331
+ setNotice(`requesting task "${id}" by prompt refused: task is finished`);
332
+ return;
333
+ }
334
+ const choices = requestNames(agents);
335
+ const ignored = choices.filter((choice) => requestPrompt(agents, choice) === "ignored");
336
+ const draft = requestDrafts.current.get(id);
337
+ const name = draft?.name ?? choices[0] ?? "";
338
+ const focus = name === "" ? "name" : ignored.includes(name) ? "request" : "prompt";
339
+ setPrompt({ id, text: draft?.text ?? "", pos: draft?.pos ?? 0, focus, request: { name, choices, ignored, typed: "" } });
340
+ };
341
+ /* cancel the prompt popup, keeping its prompt as the draft of the task (even if
342
+ empty, so that a cleared prompt is not prefilled again on reopening) */
343
+ const cancelPrompt = () => {
344
+ if (prompt === null)
345
+ return;
346
+ if (prompt.request !== null) {
347
+ requestDrafts.current.set(prompt.id, { name: prompt.request.name, text: prompt.text, pos: prompt.pos });
348
+ setNotice(`requesting task "${prompt.id}" by prompt cancelled`);
349
+ }
350
+ else {
351
+ promptDrafts.current.set(prompt.id, { text: prompt.text, pos: prompt.pos });
352
+ setNotice(`editing task "${prompt.id}" by prompt cancelled`);
353
+ }
354
+ setPrompt(null);
355
+ };
356
+ /* request a task by the prompt of its popup under its request name (processed by a
357
+ request agent of a definition, resp. by "ase agent editor" for "edit"), dropping
358
+ the draft of the task once sent, resp. keeping the request as its draft on failure,
359
+ where the prompt may be empty only for the requests of agents not requiring their prompts,
360
+ and is dropped for the requests of agents ignoring their prompts */
361
+ const submitRequest = (id, text, pos, name) => {
362
+ let requests;
363
+ try {
364
+ requests = requestsOf(name);
365
+ }
366
+ catch (err) {
367
+ setNotice(`requesting task "${id}" by prompt failed: ${err instanceof Error ? err.message : String(err)}`);
368
+ return;
369
+ }
370
+ const use = requestPrompt(agents, name);
371
+ if (text.trim() === "" && use === "required") {
372
+ setNotice(`requesting "${name}" of task "${id}" failed: empty prompt`);
373
+ return;
374
+ }
375
+ setPrompt(null);
376
+ track({ text: `requesting "${name}" of task`, id }, requests.request(log, id, use === "ignored" ? "" : text)).then(() => {
377
+ requestDrafts.current.delete(id);
378
+ setNotice(`"${name}" of task "${id}" requested`);
379
+ }).catch((err) => {
380
+ requestDrafts.current.set(id, { name, text, pos });
381
+ setNotice(`requesting "${name}" of task "${id}" failed: ${err instanceof Error ? err.message : String(err)}`);
382
+ });
383
+ };
384
+ /* request the edit of a task by the prompt of its popup (processed by "ase agent editor"),
385
+ dropping the draft of the task once sent, resp. keeping the prompt as its draft on failure,
386
+ or request a task by the prompt of its popup under its request name */
387
+ const submitPrompt = () => {
388
+ if (prompt === null)
389
+ return;
390
+ const { id, text, pos } = prompt;
391
+ if (prompt.request !== null) {
392
+ submitRequest(id, text, pos, prompt.request.name);
393
+ return;
394
+ }
395
+ if (text.trim() === "") {
396
+ setNotice(`requesting edit of task "${id}" failed: empty edit prompt`);
397
+ return;
398
+ }
399
+ setPrompt(null);
400
+ track({ text: "requesting edit of task", id }, requestEdit(log, id, text)).then(() => {
401
+ promptDrafts.current.delete(id);
402
+ setNotice(`edit of task "${id}" requested`);
403
+ }).catch((err) => {
404
+ promptDrafts.current.set(id, { text, pos });
405
+ setNotice(`requesting edit of task "${id}" failed: ${err instanceof Error ? err.message : String(err)}`);
406
+ });
407
+ };
408
+ return { startPrompt, startRequest, cancelPrompt, submitPrompt };
409
+ };
225
410
  /* the state of the terminal board: all state, the derived values, and the actions */
226
411
  export const useBoardState = (log, initial, relay) => {
227
412
  const { exit, suspendTerminal } = useApp();
@@ -262,10 +447,6 @@ export const useBoardState = (log, initial, relay) => {
262
447
  const [grow, setGrow] = React.useState(false);
263
448
  const editing = React.useRef(false);
264
449
  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
450
  /* the busy popup of slow task store operations */
270
451
  const { busy, busyTick, track } = useBusy();
271
452
  /* the rendered boxes for the mouse hit-testing */
@@ -343,13 +524,7 @@ export const useBoardState = (log, initial, relay) => {
343
524
  }
344
525
  }, [all, prompt]);
345
526
  /* 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]);
527
+ const places = React.useMemo(() => placesOf(layout?.graph), [layout]);
353
528
  /* follow changes of the task storage and of the lifecycle mode,
354
529
  and the kind and connection state of the task store
355
530
  (in the background, i.e. without the busy popup) */
@@ -359,7 +534,9 @@ export const useBoardState = (log, initial, relay) => {
359
534
  };
360
535
  const stop = watchTasks(log, refresh, setStore);
361
536
  return () => {
362
- stop().catch(() => { });
537
+ stop().catch((err) => {
538
+ log.write("warning", `board: stopping task watcher failed: ${err instanceof Error ? err.message : String(err)}`);
539
+ });
363
540
  };
364
541
  }, [log]);
365
542
  /* follow the agents passed by the agent relay (if any, else null, and undefined until
@@ -416,13 +593,7 @@ export const useBoardState = (log, initial, relay) => {
416
593
  if (!flow.cards.has(flowSel) && !flow.states.some((s) => flowState(s) === flowSel) && flow.cards.size > 0)
417
594
  setFlowSel(flow.cards.keys().next().value);
418
595
  }, [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]);
596
+ const flowPlaces = React.useMemo(() => placesOf(flowLayout?.graph), [flowLayout]);
426
597
  /* fetch the AI usage of all tasks whenever the statistics view is shown or the board
427
598
  changes, but only while the statistics view is shown (as each fetching loads all
428
599
  plans), and once known in the background, i.e. without the busy popup */
@@ -705,122 +876,8 @@ export const useBoardState = (log, initial, relay) => {
705
876
  React.useEffect(() => {
706
877
  promptLoad.current++;
707
878
  }, [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
- };
879
+ /* the actions of the prompt popup (with the drafts of its prompts and requests, per task) */
880
+ const { startPrompt, startRequest, cancelPrompt, submitPrompt } = usePromptActions({ log, all, cycle, agents, prompt, setPrompt, setNotice, track, promptLoad });
824
881
  /* toggle a flag (task titles, key hints, or standalone tasks) of the TUI surface */
825
882
  const toggleFlag = (flag) => {
826
883
  BoardState.toggleFlag("tui", flag).then((state) => {
@@ -844,6 +901,7 @@ export const useBoardState = (log, initial, relay) => {
844
901
  if (grow && !growable)
845
902
  setGrow(false);
846
903
  }, [grow, growable]);
904
+ /* the heights of the header, footer, and board area, and the inner board width */
847
905
  const headH = 3;
848
906
  const footH = surface.keys ? (view === "agents" || view === "stats" || view === "flow" ? 5 : view === "lanes" ? 7 : 6) : 3;
849
907
  const boardH = rows - headH - footH;
@@ -947,52 +1005,8 @@ export const useBoardState = (log, initial, relay) => {
947
1005
  };
948
1006
  }, [pulsing]);
949
1007
  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;
1008
+ /* the wall-clock and the heartbeats of the agents (for the agents view and the agent graph) */
1009
+ const { now, beatOf } = useBeats(agents, view);
996
1010
  return {
997
1011
  log, exit, columns, rows, all, board, surface, view, setView, filter, setFilter, typing, setTyping,
998
1012
  sel, setSel, dialog, setDialog, scroll, layout, notice, setNotice, carry, setCarry, cycle,
@@ -56,7 +56,7 @@ const wrap = (line, width) => {
56
56
  return out;
57
57
  };
58
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, "");
59
+ export const sanitize = (text) => text.replace(/\t/g, " ").replace(/[\p{Cc}\u{061C}\u{200E}\u{200F}\u{202A}-\u{202E}\u{2066}-\u{2069}]/gu, "");
60
60
  /* mark the checkbox of a plan line in the read dialog: checkboxes of items
61
61
  which are done, in progress, or open are accented, while checkboxes of
62
62
  items which are questioned, delegated, or dropped are signaled */
@@ -344,8 +344,7 @@ export const promptNext = (prompt) => promptIgnored(prompt) ? "request" : "promp
344
344
  wrapped onto the five lines of the frame of an input box (like in the web board, in
345
345
  accent color, resp. in signal color while focused), scrolled to show the cursor, with
346
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 */
347
+ focused), and with every inner cell written (with spaces) to hide the content underneath */
349
348
  export const renderPrompt = (prompt, columns, rows, choices = []) => {
350
349
  const { id, text, pos, request } = prompt;
351
350
  const box = promptBox(columns, rows, request !== null);