@rse/ase 2.0.2 → 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 (135) 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 +2 -0
  12. package/dst/ase-hook.js +12 -12
  13. package/dst/ase-lib-daemon-cli.js +117 -4
  14. package/dst/ase-lib-server-cli.js +4 -5
  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-flow.js +7 -6
  25. package/dst/ase-task-board-graph.js +5 -4
  26. package/dst/ase-task-board-tui-control.js +41 -35
  27. package/dst/ase-task-board-tui-model.js +185 -167
  28. package/dst/ase-task-board-tui-popup.js +2 -3
  29. package/dst/ase-task-board-tui-view.js +74 -70
  30. package/dst/ase-task-board-web-client.js +2 -2
  31. package/dst/ase-task-board-web-server.js +2 -2
  32. package/dst/ase-task-board.js +1 -1
  33. package/dst/ase-task-format.js +1 -1
  34. package/dst/ase-task-store-client.js +17 -5
  35. package/dst/ase-task-store-core.js +5 -7
  36. package/dst/ase-task-store-issues.js +16 -1
  37. package/dst/ase-task-store-plugin-ase.js +7 -3
  38. package/dst/ase-task-store-plugin-gitea.js +9 -32
  39. package/dst/ase-task-store-plugin-github.js +3 -12
  40. package/dst/ase-task-store-plugin-gitlab.js +2 -18
  41. package/dst/ase-task.js +51 -23
  42. package/dst/ase-util-compat.js +1 -1
  43. package/dst/ase-util-diagram.js +13 -37
  44. package/dst/ase.js +5 -5
  45. package/dst/man/ase-agent-adhoc.1 +1 -1
  46. package/dst/man/ase-agent-condenser.1 +1 -1
  47. package/dst/man/ase-agent-editor.1 +1 -1
  48. package/dst/man/ase-agent-estimator.1 +1 -1
  49. package/dst/man/ase-agent-fleet.1 +1 -1
  50. package/dst/man/ase-agent-implementer.1 +1 -1
  51. package/dst/man/ase-agent-integrator.1 +1 -1
  52. package/dst/man/ase-agent-qualifier.1 +1 -1
  53. package/dst/man/ase-agent-rebooter.1 +1 -1
  54. package/dst/man/ase-agent-relay.1 +1 -1
  55. package/dst/man/ase-agent-researcher.1 +1 -1
  56. package/dst/man/ase-agent.1 +1 -1
  57. package/dst/man/ase-artifact-list.1 +1 -1
  58. package/dst/man/ase-artifact-name.1 +1 -1
  59. package/dst/man/ase-artifact.1 +1 -1
  60. package/dst/man/ase-config-delete.1 +1 -1
  61. package/dst/man/ase-config-edit.1 +1 -1
  62. package/dst/man/ase-config-get.1 +1 -1
  63. package/dst/man/ase-config-init.1 +1 -1
  64. package/dst/man/ase-config-list.1 +1 -1
  65. package/dst/man/ase-config-set.1 +1 -1
  66. package/dst/man/ase-config.1 +1 -1
  67. package/dst/man/ase-hook-permission-request.1 +1 -1
  68. package/dst/man/ase-hook-pre-tool-use.1 +1 -1
  69. package/dst/man/ase-hook-session-end.1 +1 -1
  70. package/dst/man/ase-hook-session-persona.1 +1 -1
  71. package/dst/man/ase-hook-session-start.1 +1 -1
  72. package/dst/man/ase-hook-stop.1 +1 -1
  73. package/dst/man/ase-hook-user-prompt-submit.1 +1 -1
  74. package/dst/man/ase-hook.1 +1 -1
  75. package/dst/man/ase-man.1 +1 -1
  76. package/dst/man/ase-mcp.1 +1 -1
  77. package/dst/man/ase-service-log.1 +1 -1
  78. package/dst/man/ase-service-send.1 +1 -1
  79. package/dst/man/ase-service-start.1 +1 -1
  80. package/dst/man/ase-service-status.1 +1 -1
  81. package/dst/man/ase-service-stop.1 +1 -1
  82. package/dst/man/ase-service.1 +1 -1
  83. package/dst/man/ase-setup-disable.1 +1 -1
  84. package/dst/man/ase-setup-enable.1 +1 -1
  85. package/dst/man/ase-setup-install.1 +1 -1
  86. package/dst/man/ase-setup-mcp.1 +1 -1
  87. package/dst/man/ase-setup-status.1 +1 -1
  88. package/dst/man/ase-setup-statusline.1 +1 -1
  89. package/dst/man/ase-setup-telemetry.1 +1 -1
  90. package/dst/man/ase-setup-uninstall.1 +1 -1
  91. package/dst/man/ase-setup-update.1 +1 -1
  92. package/dst/man/ase-setup.1 +1 -1
  93. package/dst/man/ase-spec-export.1 +1 -1
  94. package/dst/man/ase-spec-lint.1 +1 -1
  95. package/dst/man/ase-spec-preview.1 +1 -1
  96. package/dst/man/ase-spec.1 +1 -1
  97. package/dst/man/ase-statusline.1 +1 -1
  98. package/dst/man/ase-task-assign.1 +1 -1
  99. package/dst/man/ase-task-board.1 +1 -1
  100. package/dst/man/ase-task-delete.1 +1 -1
  101. package/dst/man/ase-task-edit.1 +1 -1
  102. package/dst/man/ase-task-idscheme.1 +1 -1
  103. package/dst/man/ase-task-lifecycle.1 +1 -1
  104. package/dst/man/ase-task-list.1 +1 -1
  105. package/dst/man/ase-task-load.1 +1 -1
  106. package/dst/man/ase-task-newid.1 +1 -1
  107. package/dst/man/ase-task-purge.1 +1 -1
  108. package/dst/man/ase-task-rename.1 +1 -1
  109. package/dst/man/ase-task-request.1 +1 -1
  110. package/dst/man/ase-task-save.1 +1 -1
  111. package/dst/man/ase-task-status.1 +1 -1
  112. package/dst/man/ase-task-store.1 +1 -1
  113. package/dst/man/ase-task-view.1 +1 -1
  114. package/dst/man/ase-task.1 +1 -1
  115. package/dst/man/ase-util-compat.1 +1 -1
  116. package/dst/man/ase-util-diagram.1 +1 -1
  117. package/dst/man/ase-util-meta.1 +1 -1
  118. package/dst/man/ase-util-metric.1 +1 -1
  119. package/dst/man/ase-util-mint.1 +1 -1
  120. package/dst/man/ase-util-worktree.1 +1 -1
  121. package/dst/man/ase-util.1 +1 -1
  122. package/dst/man/ase.1 +1 -1
  123. package/package.json +1 -1
  124. package/plugin/.claude-plugin/plugin.json +1 -1
  125. package/plugin/.codex-plugin/plugin.json +1 -1
  126. package/plugin/.github/plugin/plugin.json +1 -1
  127. package/plugin/meta/ase-skill.md +7 -0
  128. package/plugin/mods/ase-mods.ts +10 -7
  129. package/plugin/skills/ase-code-analyze/SKILL.md +18 -10
  130. package/plugin/skills/ase-code-analyze/help.md +11 -6
  131. package/plugin/skills/ase-code-lint/SKILL.md +37 -23
  132. package/plugin/skills/ase-code-lint/help.md +10 -5
  133. package/plugin/skills/ase-help-intent/data.md +24 -12
  134. package/plugin/skills/ase-repo-review/SKILL.md +5 -3
  135. package/plugin/skills/ase-repo-review/help.md +3 -1
@@ -230,6 +230,183 @@ const useAgentsView = (agents, columns, boardH) => {
230
230
  };
231
231
  return { agentList, agentIdx, agent, agentOp, logLines, logH, logFirst, logMax, selectAgent, scrollLog, focusAgent };
232
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
+ };
233
410
  /* the state of the terminal board: all state, the derived values, and the actions */
234
411
  export const useBoardState = (log, initial, relay) => {
235
412
  const { exit, suspendTerminal } = useApp();
@@ -270,10 +447,6 @@ export const useBoardState = (log, initial, relay) => {
270
447
  const [grow, setGrow] = React.useState(false);
271
448
  const editing = React.useRef(false);
272
449
  const drafts = React.useRef(new Map());
273
- /* the drafts of the prompts of cancelled (or failed to be sent) edit requests, per task */
274
- const promptDrafts = React.useRef(new Map());
275
- /* the drafts of the cancelled (or failed to be sent) requests by prompt, per task */
276
- const requestDrafts = React.useRef(new Map());
277
450
  /* the busy popup of slow task store operations */
278
451
  const { busy, busyTick, track } = useBusy();
279
452
  /* the rendered boxes for the mouse hit-testing */
@@ -361,7 +534,9 @@ export const useBoardState = (log, initial, relay) => {
361
534
  };
362
535
  const stop = watchTasks(log, refresh, setStore);
363
536
  return () => {
364
- stop().catch(() => { });
537
+ stop().catch((err) => {
538
+ log.write("warning", `board: stopping task watcher failed: ${err instanceof Error ? err.message : String(err)}`);
539
+ });
365
540
  };
366
541
  }, [log]);
367
542
  /* follow the agents passed by the agent relay (if any, else null, and undefined until
@@ -701,122 +876,8 @@ export const useBoardState = (log, initial, relay) => {
701
876
  React.useEffect(() => {
702
877
  promptLoad.current++;
703
878
  }, [confirm, transfer, assign, prompt, dialog?.id, sel.id, view]);
704
- /* open the prompt popup of an edit request of a task (refused for a finished task, and
705
- while an edit of it is in progress, as a new request would change the task under the
706
- running edit), prefilled with its draft of a cancelled (or failed to be sent) prompt,
707
- else with the prompt of its most recent failed edit request (for a retry) */
708
- const startPrompt = (id) => {
709
- const card = all.cards.get(id);
710
- if (card === undefined)
711
- return;
712
- if (cycle !== null && editRefused(cycle, card.actual)) {
713
- setNotice(`editing task "${id}" by prompt refused: task is finished`);
714
- return;
715
- }
716
- if (card.edit === "active") {
717
- setNotice(`editing task "${id}" by prompt refused: edit in progress`);
718
- return;
719
- }
720
- const draft = promptDrafts.current.get(id);
721
- if (draft !== undefined) {
722
- setPrompt({ id, ...draft, focus: "prompt", request: null });
723
- return;
724
- }
725
- const token = ++promptLoad.current;
726
- track({ text: "loading edit prompt of task", id }, editPrompt(log, id)).then((text) => {
727
- if (token === promptLoad.current)
728
- setPrompt({ id, text, pos: [...text].length, focus: "prompt", request: null });
729
- }).catch((err) => {
730
- if (token === promptLoad.current)
731
- setNotice(`editing task "${id}" by prompt failed: ${err instanceof Error ? err.message : String(err)}`);
732
- });
733
- };
734
- /* open the prompt popup of a request by prompt of any name of a task (see "ase task
735
- request", refused for a finished task), with the request name suggested from the
736
- request agents announced to the agent relay, prefilled with its draft of a cancelled
737
- (or failed to be sent) request, where a missing request name is to be entered first */
738
- const startRequest = (id) => {
739
- const card = all.cards.get(id);
740
- if (card === undefined)
741
- return;
742
- if (cycle !== null && editRefused(cycle, card.actual)) {
743
- setNotice(`requesting task "${id}" by prompt refused: task is finished`);
744
- return;
745
- }
746
- const choices = requestNames(agents);
747
- const ignored = choices.filter((choice) => requestPrompt(agents, choice) === "ignored");
748
- const draft = requestDrafts.current.get(id);
749
- const name = draft?.name ?? choices[0] ?? "";
750
- const focus = name === "" ? "name" : ignored.includes(name) ? "request" : "prompt";
751
- setPrompt({ id, text: draft?.text ?? "", pos: draft?.pos ?? 0, focus, request: { name, choices, ignored, typed: "" } });
752
- };
753
- /* cancel the prompt popup, keeping its prompt as the draft of the task (even if
754
- empty, so that a cleared prompt is not prefilled again on reopening) */
755
- const cancelPrompt = () => {
756
- if (prompt === null)
757
- return;
758
- if (prompt.request !== null) {
759
- requestDrafts.current.set(prompt.id, { name: prompt.request.name, text: prompt.text, pos: prompt.pos });
760
- setNotice(`requesting task "${prompt.id}" by prompt cancelled`);
761
- }
762
- else {
763
- promptDrafts.current.set(prompt.id, { text: prompt.text, pos: prompt.pos });
764
- setNotice(`editing task "${prompt.id}" by prompt cancelled`);
765
- }
766
- setPrompt(null);
767
- };
768
- /* request a task by the prompt of its popup under its request name (processed by a
769
- request agent of a definition, resp. by "ase agent editor" for "edit"), dropping
770
- the draft of the task once sent, resp. keeping the request as its draft on failure,
771
- where the prompt may be empty only for the requests of agents not requiring their prompts,
772
- and is dropped for the requests of agents ignoring their prompts */
773
- const submitRequest = (id, text, pos, name) => {
774
- let requests;
775
- try {
776
- requests = requestsOf(name);
777
- }
778
- catch (err) {
779
- setNotice(`requesting task "${id}" by prompt failed: ${err instanceof Error ? err.message : String(err)}`);
780
- return;
781
- }
782
- const use = requestPrompt(agents, name);
783
- if (text.trim() === "" && use === "required") {
784
- setNotice(`requesting "${name}" of task "${id}" failed: empty prompt`);
785
- return;
786
- }
787
- setPrompt(null);
788
- track({ text: `requesting "${name}" of task`, id }, requests.request(log, id, use === "ignored" ? "" : text)).then(() => {
789
- requestDrafts.current.delete(id);
790
- setNotice(`"${name}" of task "${id}" requested`);
791
- }).catch((err) => {
792
- requestDrafts.current.set(id, { name, text, pos });
793
- setNotice(`requesting "${name}" of task "${id}" failed: ${err instanceof Error ? err.message : String(err)}`);
794
- });
795
- };
796
- /* request the edit of a task by the prompt of its popup (processed by "ase agent editor"),
797
- dropping the draft of the task once sent, resp. keeping the prompt as its draft on failure,
798
- or request a task by the prompt of its popup under its request name */
799
- const submitPrompt = () => {
800
- if (prompt === null)
801
- return;
802
- const { id, text, pos } = prompt;
803
- if (prompt.request !== null) {
804
- submitRequest(id, text, pos, prompt.request.name);
805
- return;
806
- }
807
- if (text.trim() === "") {
808
- setNotice(`requesting edit of task "${id}" failed: empty edit prompt`);
809
- return;
810
- }
811
- setPrompt(null);
812
- track({ text: "requesting edit of task", id }, requestEdit(log, id, text)).then(() => {
813
- promptDrafts.current.delete(id);
814
- setNotice(`edit of task "${id}" requested`);
815
- }).catch((err) => {
816
- promptDrafts.current.set(id, { text, pos });
817
- setNotice(`requesting edit of task "${id}" failed: ${err instanceof Error ? err.message : String(err)}`);
818
- });
819
- };
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 });
820
881
  /* toggle a flag (task titles, key hints, or standalone tasks) of the TUI surface */
821
882
  const toggleFlag = (flag) => {
822
883
  BoardState.toggleFlag("tui", flag).then((state) => {
@@ -840,6 +901,7 @@ export const useBoardState = (log, initial, relay) => {
840
901
  if (grow && !growable)
841
902
  setGrow(false);
842
903
  }, [grow, growable]);
904
+ /* the heights of the header, footer, and board area, and the inner board width */
843
905
  const headH = 3;
844
906
  const footH = surface.keys ? (view === "agents" || view === "stats" || view === "flow" ? 5 : view === "lanes" ? 7 : 6) : 3;
845
907
  const boardH = rows - headH - footH;
@@ -943,52 +1005,8 @@ export const useBoardState = (log, initial, relay) => {
943
1005
  };
944
1006
  }, [pulsing]);
945
1007
  const pulse = pulsing ? pulseTick : -1;
946
- /* tick the wall-clock every second while the agents view is shown, for the live
947
- durations of its footer (the time in the current state and the age of the heartbeat) */
948
- const [now, setNow] = React.useState(Date.now());
949
- React.useEffect(() => {
950
- if (view !== "agents")
951
- return;
952
- setNow(Date.now());
953
- const timer = setInterval(() => {
954
- setNow(Date.now());
955
- }, 1000);
956
- return () => {
957
- clearInterval(timer);
958
- };
959
- }, [view]);
960
- /* the local time at which the latest heartbeat of each agent was received (measured
961
- locally, as the clock of the agent relay may be skewed), for the heart of its card
962
- (filled for BEAT_SHOWN_MS after a heartbeat, else empty), where the heartbeats are
963
- recognized by their changed time stamps and a filled heart is re-rendered once it is
964
- due to be emptied (as the wall-clock tick alone would empty it too early or too late) */
965
- const beats = React.useRef(new Map());
966
- const beatsSeen = React.useMemo(() => {
967
- const at = Date.now();
968
- const list = agents?.agents ?? [];
969
- const next = new Map();
970
- for (const a of list) {
971
- const prev = beats.current.get(a.id);
972
- next.set(a.id, prev !== undefined && prev.beat === a.beat ? prev : { beat: a.beat, seen: at });
973
- }
974
- beats.current = next;
975
- return next;
976
- }, [agents]);
977
- React.useEffect(() => {
978
- if (view !== "agents" && view !== "flow")
979
- return;
980
- const latest = Math.max(0, ...[...beatsSeen.values()].map((b) => b.seen));
981
- const due = latest + BEAT_SHOWN_MS - Date.now();
982
- if (latest === 0 || due <= 0)
983
- return;
984
- const timer = setTimeout(() => {
985
- setNow(Date.now());
986
- }, due);
987
- return () => {
988
- clearTimeout(timer);
989
- };
990
- }, [view, beatsSeen]);
991
- 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);
992
1010
  return {
993
1011
  log, exit, columns, rows, all, board, surface, view, setView, filter, setFilter, typing, setTyping,
994
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);