@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
@@ -19,8 +19,12 @@ const GROUP_GAP = 1;
19
19
  /* layout constants of a grown lane: minimum card width, gap between cards */
20
20
  const GRID_MIN = 30;
21
21
  const GRID_GAP = 1;
22
- /* make a text of the agent relay safe for the terminal (strip control characters) */
23
- const clean = (text) => text.replace(/\p{Cc}/gu, "");
22
+ /* make a text of the agent relay safe for the terminal (strip control and bidirectional formatting characters) */
23
+ const clean = (text) => text.replace(/[\p{Cc}\u{061C}\u{200E}\u{200F}\u{202A}-\u{202E}\u{2066}-\u{2069}]/gu, "");
24
+ /* append to the content of an info line (" " if empty) the hint how to show the
25
+ hidden key hints again, so it need not be remembered while the key hints are hidden */
26
+ const keysHint = (surface, info) => surface.keys ? info : info === " " ? "?: show key hints" :
27
+ [...(Array.isArray(info) ? info : [info]), " · ?: show key hints"];
24
28
  /* compute the visible group range starting at a first group, with the
25
29
  width of each visible group (collapsed groups stay narrow, expanded
26
30
  groups share the remaining width but never drop below the minimum) */
@@ -35,8 +39,7 @@ export const fitGroups = (groups, collapsed, first, width) => {
35
39
  widths.push(w);
36
40
  used += gap + w;
37
41
  }
38
- const expanded = widths.map((w, i) => w === GROUP_COLL && collapsed.includes(groups[first + i].title) ? -1 : i)
39
- .filter((i) => i >= 0);
42
+ const expanded = [...widths.keys()].filter((i) => !collapsed.includes(groups[first + i].title));
40
43
  if (expanded.length > 0) {
41
44
  const extra = Math.max(0, width - used);
42
45
  const each = Math.floor(extra / expanded.length);
@@ -331,7 +334,8 @@ export const renderLanes = (ctx) => {
331
334
  const arrows = shown < total && !grown;
332
335
  const group = board.groups[sel.g];
333
336
  const info = grown ? `lane ${group.lanes[sel.l].status} of group ${group.title} grown · g shrinks` :
334
- `groups ${fit.first + 1}–${fit.last + 1} of ${total} · ${arrows ? "←/→ scrolls" : "all visible"}`;
337
+ `groups ${fit.first + 1}–${fit.last + 1} of ${total} · ${arrows ? "←/→ scrolls" : "all visible"}` +
338
+ (surface.keys ? "" : " · ?: show key hints");
335
339
  const view = { board, surface, sel, dim, carry, titles: surface.titles, scroll: { left: -1, right: -1 }, pulse, cardRef, laneRef, groupRef, agents, working };
336
340
  /* the scroll bar consumes the entire width remaining besides the info */
337
341
  const track = Math.max(1, columns - 2 - info.length - 3);
@@ -390,8 +394,8 @@ export const renderGraph = (ctx) => {
390
394
  })))))),
391
395
  /* overlay the graph statistics centered onto the top border */
392
396
  h(Box, { position: "absolute", top: 0, left: 0, width: "100%", justifyContent: "center" }, h(Text, { ...cx("dim", dim && "dimmed"), wrap: "truncate" }, `[ ${board.cards.size} tasks, ${deps} dependencies ]`))),
393
- h(Box, { key: "info", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), card === undefined ? " " :
394
- ["task: ", h(Text, { key: "id", ...cx("bold") }, card.id), " · status: ", h(Text, { key: "status", ...cx("bold") }, card.status)])),
397
+ h(Box, { key: "info", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), keysHint(surface, card === undefined ? " " :
398
+ ["task: ", h(Text, { key: "id", ...cx("bold") }, card.id), " · status: ", h(Text, { key: "status", ...cx("bold") }, card.status)]))),
395
399
  h(Box, { key: "keys1", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), "↑/↓/←/→/Left-Click: select task · ⏎/Double-Click: view task · e: edit task · T: transition task")),
396
400
  h(Box, { key: "keys2", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), `D: delete task · A: assign task · E: edit by prompt · R: request by prompt · N: new task · t: ${graphTitles ? "collapse" : "expand"} titles · ` +
397
401
  `s: ${surface.standalone ? "hide" : "show"} standalone tasks`)),
@@ -434,7 +438,7 @@ export const renderFlow = (ctx) => {
434
438
  })))))),
435
439
  /* overlay the graph statistics centered onto the top border */
436
440
  h(Box, { position: "absolute", top: 0, left: 0, width: "100%", justifyContent: "center" }, h(Text, { ...cx("dim", dim && "dimmed"), wrap: "truncate" }, `[ ${agents?.agents.length ?? 0} agents, ${flow.states.length} states ]`))),
437
- h(Box, { key: "info", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), rel !== null ? [
441
+ h(Box, { key: "info", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), keysHint(ctx.surface, rel !== null ? [
438
442
  "state: ", h(Text, { key: "state", ...cx("bold") }, rel.state),
439
443
  " · observed by: ", h(Text, { key: "in", ...cx("bold") }, names(rel.input)),
440
444
  " · written by: ", h(Text, { key: "out", ...cx("bold") }, names(rel.output)),
@@ -448,7 +452,7 @@ export const renderFlow = (ctx) => {
448
452
  " · output: ", h(Text, { key: "out", ...cx("bold") }, states.output.join(", ") || "none"),
449
453
  ...(states.gate !== undefined ? [" · gate: ", h(Text, { key: "gate", ...cx("bold") }, states.gate)] : [])
450
454
  ])
451
- ])),
455
+ ]))),
452
456
  h(Box, { key: "keys1", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), "↑/↓/←/→/Left-Click: select agent/state · ⏎/Double-Click: view agent · " +
453
457
  "⇈/⇊/⇤/⇥/Wheel: scroll canvas")),
454
458
  h(Box, { key: "keys2", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), `v/V: ${viewCycleHint("flow", agents !== null)} · M: ${mouse ? "disable" : "enable"} mouse · ?: hide key hints · q: quit`))
@@ -530,22 +534,22 @@ export const renderAgents = (ctx) => {
530
534
  const thumbH = Math.max(1, Math.round(logH * Math.min(1, logH / Math.max(1, logLines.length))));
531
535
  const thumbY = logMax === 0 ? 0 : Math.round((logH - thumbH) * logFirst / logMax);
532
536
  const field = (name, value, width, wrap = "truncate") => h(Box, { key: name }, h(Box, { flexShrink: 0 }, h(Text, cx("accent", dim && "dimmed"), name.padEnd(width))), h(Box, { flexGrow: 1, flexShrink: 1 }, h(Text, { ...cx("accent", "bold", dim && "dimmed"), wrap }, value)));
533
- const pane = (key, text, fields, first = false, accent = false) => h(Box, { key, flexGrow: 1, flexBasis: 0, marginLeft: first ? 0 : 1 }, h(Box, { paddingX: 1, ...cx("grow", "column", "frame", accent ? "border-accent" : "border-dim", dim && "border-dimmed") }, ...(agent === undefined ? [h(Text, { key: "none", ...cx("dim", "truncate", dim && "dimmed") }, empty)] : fields())), title(text, accent));
537
+ const pane = (key, text, fields, first = false, accent = false) => h(Box, { key, flexGrow: 1, flexBasis: 0, marginLeft: first ? 0 : 1 }, h(Box, { paddingX: 1, ...cx("grow", "column", "frame", accent ? "border-accent" : "border-dim", dim && "border-dimmed") }, ...(agent === undefined ? [h(Text, { key: "none", ...cx("dim", "truncate", dim && "dimmed") }, empty)] : fields(agent))), title(text, accent));
534
538
  const op = h(Box, { key: "op", height: agentOpHeight(agentOp), flexShrink: 0 }, pane("operation", "Operation", () => [
535
539
  h(Text, { key: "title", ...cx("normal", "truncate", dim && "dimmed") }, h(Text, cx("normal", "badge", dim && "dimmed"), ` ⚉ ${agentOp.name} `), " ", agentOp.title !== "" ? h(Text, cx("bold"), agentOp.title) :
536
540
  agentOp.desc.length === 0 ? h(Text, cx("dim"), "(no operation title and description)") : ""),
537
541
  ...agentOp.desc.map((line, i) => h(Text, { key: `desc-${i}`, ...cx("normal", "truncate", dim && "dimmed") }, line))
538
542
  ], true));
539
- const info = h(Box, { key: "info", height: AGENT_INFO_H, flexShrink: 0 }, pane("process", "Process", () => [
540
- field("host:", clean(agent.host), 6),
541
- field("pid:", String(agent.pid), 6),
542
- field("uid:", clean(agentUser(agent)), 6),
543
- field("cwd:", clean(agent.cwd ?? "n/a"), 6, "truncate-start")
544
- ], true, true), pane("accounting", "Accounting", () => [
545
- field("uptime:", agentUptime(agent), 18),
546
- field("heartbeat:", `${agentDuration(now - agent.beat)} ago`, 18),
547
- field("usage (total):", agentUsage(agent.usage?.total), 18),
548
- field("usage (last job):", agentUsage(agent.usage?.last), 18)
543
+ const info = h(Box, { key: "info", height: AGENT_INFO_H, flexShrink: 0 }, pane("process", "Process", (a) => [
544
+ field("host:", clean(a.host), 6),
545
+ field("pid:", String(a.pid), 6),
546
+ field("uid:", clean(agentUser(a)), 6),
547
+ field("cwd:", clean(a.cwd ?? "n/a"), 6, "truncate-start")
548
+ ], true, true), pane("accounting", "Accounting", (a) => [
549
+ field("uptime:", agentUptime(a), 18),
550
+ field("heartbeat:", `${agentDuration(now - a.beat)} ago`, 18),
551
+ field("usage (total):", agentUsage(a.usage?.total), 18),
552
+ field("usage (last job):", agentUsage(a.usage?.last), 18)
549
553
  ], false, true));
550
554
  const log = h(Box, { key: "log", flexGrow: 1 }, h(Box, { paddingLeft: 1, ...cx("grow", "column", "frame", "border-dim", dim && "border-dimmed") }, ...(agent === undefined ? [h(Text, { key: "none", ...cx("dim", dim && "dimmed") }, empty)] :
551
555
  lines.length === 0 ? [h(Text, { key: "none", ...cx("dim", dim && "dimmed") }, "(no processing log lines yet)")] :
@@ -554,19 +558,19 @@ export const renderAgents = (ctx) => {
554
558
  const detail = h(Box, { key: "detail", flexGrow: 1, height: boardH, marginLeft: 1, flexDirection: "column" }, op, info, log);
555
559
  return [
556
560
  h(Box, { key: "agents", height: boardH, paddingX: 1 }, master, detail),
557
- h(Box, { key: "info", ...cx("bar") }, h(Text, cx("hint", "truncate", dim && "dimmed"), agent === undefined ? " " : [
561
+ h(Box, { key: "info", ...cx("bar") }, h(Text, cx("hint", "truncate", dim && "dimmed"), keysHint(ctx.surface, agent === undefined ? " " : [
558
562
  "agent: ", h(Text, { key: "agent", ...cx("bold") }, clean(agentLabel(agent))),
559
563
  ...(agentOp.title !== "" ? [
560
564
  " · operation: ", h(Text, { key: "op", ...cx("bold") }, agentOp.title)
561
565
  ] : [])
562
- ])),
566
+ ]))),
563
567
  h(Box, { key: "keys1", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), "↑/↓/Left-Click: select agent · ⇈/⇊/⇤/⇥: page/top/bottom agent log · Mouse-Wheel: scroll agent log")),
564
568
  h(Box, { key: "keys2", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), `v/V: ${viewCycleHint("agents", agents !== null)} · M: ${mouse ? "disable" : "enable"} mouse · ?: hide key hints · q: quit`))
565
569
  ];
566
570
  };
567
571
  /* the heights of the buttons of the time ranges (with a blank line below them), and of the
568
- frames "Task Progress" (and "Open Tasks" besides it), "Usage" (and "Effort" besides
569
- it), and "Top-5 Tasks" (and "Finished Tasks" and "Efficiency" besides it), each
572
+ frames "Task Progress" (and "Open Tasks" beside it), "Usage" (and "Effort" beside
573
+ it), and "Top-5 Tasks" (and "Finished Tasks" and "Efficiency" beside it), each
570
574
  with an inner blank line at its top and bottom (except for the column charts), of the
571
575
  statistics view */
572
576
  const RANGES_H = 2;
@@ -614,15 +618,60 @@ const statsBar = (value, max, width) => {
614
618
  const n = max > 0 ? Math.round(value / max * width) : 0;
615
619
  return "█".repeat(n).padEnd(width, "░");
616
620
  };
621
+ /* the color of a tone of the statistics view (like the lanes) */
622
+ const statsTint = (tone) => tone === "active" ? palette.accent : tone === "done" ? palette.dim : palette.normal;
623
+ /* a framed section of a height of the statistics view (dimmed, resp. in accent color),
624
+ with its title centered onto its top border */
625
+ const statsFrame = (dim, accent) => (key, height, title, padY, ...content) => h(Box, { key, height, flexShrink: 0 }, h(Box, { paddingX: 1, paddingY: padY, ...cx("grow", "column", "frame", accent ? "border-accent" : "border-dim", dim && "border-dimmed") }, ...content), h(Box, { position: "absolute", top: 0, left: 0, width: "100%", justifyContent: "center" }, h(Text, { ...cx(accent ? "accent" : "dim", dim && "dimmed"), wrap: "truncate" }, "[ ", h(Text, cx("bold"), title), " ]")));
626
+ /* a framed column chart of an inner width of the statistics view, where the highest column leaves the top row
627
+ for its number, each number sits on top of its column (being at least a thin line,
628
+ even without any number, like in the web board), the columns stand on a blank line above
629
+ their labels (each cut to its first five characters, without an ellipsis, for
630
+ readability) and their sub-labels below them (like the effort of a state, cut with
631
+ an ellipsis, and blank if empty, to keep the columns of all charts aligned), and each
632
+ group of columns is labeled below its line spanning its columns
633
+ (like in the web board), followed by a footer line (blank if empty, to keep the columns
634
+ of all charts aligned), where the columns start directly at the left padding of the
635
+ frame, are separated by a gap of one column, and leave the remaining width on the right,
636
+ and where a compact chart of a height omits the blank line, the sub-labels, and the groups */
637
+ const statsColChart = (dim, key, title, width, cols, groups, footer, height, compact = false) => {
638
+ const frame = statsFrame(dim, false);
639
+ const areaH = Math.max(1, height - (compact ? 4 : 8));
640
+ const colW = Math.max(2, Math.floor((width + 1) / cols.length));
641
+ const lineW = colW - 1;
642
+ const fitW = Math.max(1, Math.min(14, lineW));
643
+ const barW = fitW > 1 && (lineW - fitW) % 2 !== 0 ? fitW - 1 : fitW;
644
+ const barL = Math.floor((lineW - barW) / 2);
645
+ const max = Math.max(1, ...cols.map((c) => c.count));
646
+ const gapOf = (i, n) => i < n - 1 ? " " : "";
647
+ const column = (k, tone, text) => h(Text, { key: k, color: statsTint(tone), dimColor: dim }, " ".repeat(barL) + text + " ".repeat(Math.max(0, lineW - barW - barL)) + gapOf(k, cols.length));
648
+ const rows = Array.from({ length: areaH }, (_x, r) => {
649
+ const level = areaH - 1 - r;
650
+ return h(Box, { key: `row${r}` }, h(Text, { wrap: "truncate" }, ...cols.map((c, i) => {
651
+ const fill = Math.max(1, Math.round(c.count / max * (areaH - 1) * 8));
652
+ const cell = Math.max(0, Math.min(8, fill - level * 8));
653
+ const text = String(c.count);
654
+ if (cell === 0 && level === Math.ceil(fill / 8) && text.length > barW)
655
+ return h(Text, { key: i, color: statsTint(c.tone), dimColor: dim }, center(text, lineW) + gapOf(i, cols.length));
656
+ return column(i, c.tone, cell > 0 ? lowerEighths[cell].repeat(barW) :
657
+ level === Math.ceil(fill / 8) ? center(text, barW) : " ".repeat(barW));
658
+ })));
659
+ });
660
+ const labels = h(Box, { key: "labels" }, h(Text, { wrap: "truncate" }, ...cols.map((c, i) => h(Text, { key: i, color: statsTint(c.tone), dimColor: dim, bold: true }, center(c.label.slice(0, Math.min(5, lineW)), lineW) + gapOf(i, cols.length)))));
661
+ const foot = h(Box, { key: "footer" }, h(Text, { ...cx("dim", dim && "dimmed"), wrap: "truncate" }, center(footer, Math.max(0, cols.length * colW - 1))));
662
+ if (compact)
663
+ return frame(key, height, title, 0, ...rows, labels, foot);
664
+ return frame(key, height, title, 0, ...rows, h(Box, { key: "base" }, h(Text, { wrap: "truncate" }, " ")), labels, h(Box, { key: "subs" }, h(Text, { wrap: "truncate" }, ...cols.map((c, i) => h(Text, { key: i, color: statsTint(c.tone), dimColor: dim }, center(c.sub ?? "", lineW) + gapOf(i, cols.length))))), h(Box, { key: "groups" }, h(Text, { ...cx("dim", dim && "dimmed"), wrap: "truncate" }, groups.map((g, i) => "─".repeat(Math.max(0, g.n * colW - 1)) + gapOf(i, groups.length)).join(""))), h(Box, { key: "group-labels" }, h(Text, { ...cx("dim", dim && "dimmed"), wrap: "truncate" }, groups.map((g, i) => center(g.label, g.n * colW - 1) + gapOf(i, groups.length)).join(""))), foot);
665
+ };
617
666
  /* render the statistics view: the framed column chart "Tasks per State" of the numbers of tasks per
618
667
  lifecycle state (labeled by state, its effort, and phase, in the colors of their lanes, with the framed column
619
- chart "Throughput" of the finished tasks of the last weeks besides it), the framed stacked bar
668
+ chart "Throughput" of the finished tasks of the last weeks beside it), the framed stacked bar
620
669
  "Task Progress" of the cancelled, finished, work-in-progress, and open tasks (with the framed
621
- stacked bar "Open Tasks" of the ready and blocked open tasks besides it), the framed bars "Usage"
670
+ stacked bar "Open Tasks" of the ready and blocked open tasks beside it), the framed bars "Usage"
622
671
  of the total times, tokens, and costs of the tasks (with the framed column chart "Effort"
623
- of the numbers of tasks per effort size besides it), and the framed table "Top-5 Tasks" of the
672
+ of the numbers of tasks per effort size beside it), and the framed table "Top-5 Tasks" of the
624
673
  tasks of the highest usage (with the framed "Finished Tasks" of the usage per finished task and
625
- the framed "Efficiency" of the AI usage besides it), below the buttons of the time ranges */
674
+ the framed "Efficiency" of the AI usage beside it), below the buttons of the time ranges */
626
675
  export const renderStats = (ctx) => {
627
676
  const { stats, usage, range, rangeRef, columns, boardH, dim, mouse, filter, agents } = ctx;
628
677
  const innerW = Math.max(20, columns - 6);
@@ -635,55 +684,15 @@ export const renderStats = (ctx) => {
635
684
  /* the number of columns the frames "Finished Tasks" and "Efficiency" are each grown by
636
685
  (at the expense of the frame "Top-5 Tasks") */
637
686
  const avgGrow = 4;
638
- const tint = (tone) => tone === "active" ? palette.accent : tone === "done" ? palette.dim : palette.normal;
639
- const frameOf = (accent) => (key, height, title, padY, ...content) => h(Box, { key, height, flexShrink: 0 }, h(Box, { paddingX: 1, paddingY: padY, ...cx("grow", "column", "frame", accent ? "border-accent" : "border-dim", dim && "border-dimmed") }, ...content), h(Box, { position: "absolute", top: 0, left: 0, width: "100%", justifyContent: "center" }, h(Text, { ...cx(accent ? "accent" : "dim", dim && "dimmed"), wrap: "truncate" }, "[ ", h(Text, cx("bold"), title), " ]")));
640
- const frame = frameOf(false);
641
- const accentFrame = frameOf(true);
687
+ const frame = statsFrame(dim, false);
688
+ const accentFrame = statsFrame(dim, true);
642
689
  if (stats === null)
643
690
  return [h(Box, { key: "stats", height: boardH }), h(Text, { key: "info" }, " "), h(Text, { key: "keys1" }, " "), h(Text, { key: "keys2" }, " ")];
644
- /* a framed column chart of an inner width, where the highest column leaves the top row
645
- for its number, each number sits on top of its column (being at least a thin line,
646
- even without any number, like in the web board), the columns stand on a blank line above
647
- their labels (each cut to its first five characters, without an ellipsis, for
648
- readability) and their sub-labels below them (like the effort of a state, cut with
649
- an ellipsis, and blank if empty, to keep the columns of all charts aligned), and each
650
- group of columns is labeled below its line spanning its columns
651
- (like in the web board), followed by a footer line (blank if empty, to keep the columns
652
- of all charts aligned), where the columns start directly at the left padding of the
653
- frame, are separated by a gap of one column, and leave the remaining width on the right,
654
- and where a compact chart of a height omits the blank line, the sub-labels, and the groups */
691
+ /* the height of the column charts, taking the height left by the other rows */
655
692
  const chartH = Math.max(9, boardH - RANGES_H - PROGRESS_H - USAGE_H - TOP_H);
656
- const colChart = (key, title, width, cols, groups, footer = "", height = chartH, compact = false) => {
657
- const areaH = Math.max(1, height - (compact ? 4 : 8));
658
- const colW = Math.max(2, Math.floor((width + 1) / cols.length));
659
- const lineW = colW - 1;
660
- const fitW = Math.max(1, Math.min(14, lineW));
661
- const barW = fitW > 1 && (lineW - fitW) % 2 !== 0 ? fitW - 1 : fitW;
662
- const barL = Math.floor((lineW - barW) / 2);
663
- const max = Math.max(1, ...cols.map((c) => c.count));
664
- const gapOf = (i, n) => i < n - 1 ? " " : "";
665
- const column = (k, tone, text) => h(Text, { key: k, color: tint(tone), dimColor: dim }, " ".repeat(barL) + text + " ".repeat(Math.max(0, lineW - barW - barL)) + gapOf(k, cols.length));
666
- const rows = Array.from({ length: areaH }, (_x, r) => {
667
- const level = areaH - 1 - r;
668
- return h(Box, { key: `row${r}` }, h(Text, { wrap: "truncate" }, ...cols.map((c, i) => {
669
- const fill = Math.max(1, Math.round(c.count / max * (areaH - 1) * 8));
670
- const cell = Math.max(0, Math.min(8, fill - level * 8));
671
- const text = String(c.count);
672
- if (cell === 0 && level === Math.ceil(fill / 8) && text.length > barW)
673
- return h(Text, { key: i, color: tint(c.tone), dimColor: dim }, center(text, lineW) + gapOf(i, cols.length));
674
- return column(i, c.tone, cell > 0 ? lowerEighths[cell].repeat(barW) :
675
- level === Math.ceil(fill / 8) ? center(text, barW) : " ".repeat(barW));
676
- })));
677
- });
678
- const labels = h(Box, { key: "labels" }, h(Text, { wrap: "truncate" }, ...cols.map((c, i) => h(Text, { key: i, color: tint(c.tone), dimColor: dim, bold: true }, center(c.label.slice(0, Math.min(5, lineW)), lineW) + gapOf(i, cols.length)))));
679
- const foot = h(Box, { key: "footer" }, h(Text, { ...cx("dim", dim && "dimmed"), wrap: "truncate" }, center(footer, Math.max(0, cols.length * colW - 1))));
680
- if (compact)
681
- return frame(key, height, title, 0, ...rows, labels, foot);
682
- return frame(key, height, title, 0, ...rows, h(Box, { key: "base" }, h(Text, { wrap: "truncate" }, " ")), labels, h(Box, { key: "subs" }, h(Text, { wrap: "truncate" }, ...cols.map((c, i) => h(Text, { key: i, color: tint(c.tone), dimColor: dim }, center(c.sub ?? "", lineW) + gapOf(i, cols.length))))), h(Box, { key: "groups" }, h(Text, { ...cx("dim", dim && "dimmed"), wrap: "truncate" }, groups.map((g, i) => "─".repeat(Math.max(0, g.n * colW - 1)) + gapOf(i, groups.length)).join(""))), h(Box, { key: "group-labels" }, h(Text, { ...cx("dim", dim && "dimmed"), wrap: "truncate" }, groups.map((g, i) => center(g.label, g.n * colW - 1) + gapOf(i, groups.length)).join(""))), foot);
683
- };
684
693
  /* the column chart of the number of tasks per state (labeled by the states and below by the
685
694
  effort of their estimated tasks, grouped by their phases, and with the total number of tasks
686
- as its footer), with the column chart of the throughput of the last weeks besides it (labeled by
695
+ as its footer), with the column chart of the throughput of the last weeks beside it (labeled by
687
696
  their week numbers, prefixed with "W" if fitting, and grouped by all weeks with the total
688
697
  and average number of finished tasks, and with the forecast of the backlog as its footer),
689
698
  grown like the frames "Finished Tasks" and "Efficiency" together, to align with them */
@@ -699,10 +708,10 @@ export const renderStats = (ctx) => {
699
708
  const weekW = Math.max(2, Math.floor(weeksW / Math.max(1, stats.throughput.length))) - 1;
700
709
  const weekSum = stats.throughput.reduce((n, w) => n + w.count, 0);
701
710
  const weekAvg = stats.throughput.length > 0 ? weekSum / stats.throughput.length : 0;
702
- const chart = pair("charts", chartH, colChart("chart", "Tasks per State", leftW - 2 * avgGrow - 4, stats.states.map((s) => ({
711
+ const chart = pair("charts", chartH, statsColChart(dim, "chart", "Tasks per State", leftW - 2 * avgGrow - 4, stats.states.map((s) => ({
703
712
  label: s.status, count: s.count, tone: s.tone,
704
713
  sub: s.effort.estimated > 0 ? statsHoursN(s.effort.minutes) : ""
705
- })), phases, `total tasks: ${stats.states.reduce((n, s) => n + s.count, 0)}`), colChart("throughput", "Throughput", weeksW, stats.throughput.map((w) => ({ label: weekW >= 3 ? `W${w.week}` : String(w.week), count: w.count, tone: "idle" })), [{ label: `${weekSum} finished · ⌀ ${weekAvg.toFixed(1)}/week`, n: stats.throughput.length }], statsForecast(stats.forecast.backlog, stats.forecast.weeks)), 2 * avgGrow);
714
+ })), phases, `total tasks: ${stats.states.reduce((n, s) => n + s.count, 0)}`, chartH), statsColChart(dim, "throughput", "Throughput", weeksW, stats.throughput.map((w) => ({ label: weekW >= 3 ? `W${w.week}` : String(w.week), count: w.count, tone: "idle" })), [{ label: `${weekSum} finished · ⌀ ${weekAvg.toFixed(1)}/week`, n: stats.throughput.length }], statsForecast(stats.forecast.backlog, stats.forecast.weeks), chartH), 2 * avgGrow);
706
715
  /* a framed stacked bar of the numbers of tasks of parts, with its legend */
707
716
  const stacked = (key, title, width, parts) => {
708
717
  const total = parts.reduce((n, p) => n + p.count, 0);
@@ -780,9 +789,9 @@ export const renderStats = (ctx) => {
780
789
  [h(Text, { key: "loading", ...cx("dim", dim && "dimmed") }, "loading …")] : [head, ...lines]));
781
790
  /* the compact column chart of the distribution of the effort sizes (the numbers of the
782
791
  estimated tasks per size, from the smallest to the largest size), with the numbers of
783
- the estimated and of all tasks as its footer, besides the bars of the total usage */
792
+ the estimated and of all tasks as its footer, beside the bars of the total usage */
784
793
  const effort = stats.states.reduce((sum, s) => ({ estimated: sum.estimated + s.effort.estimated, tasks: sum.tasks + s.effort.tasks }), { estimated: 0, tasks: 0 });
785
- const usages = pair("usage", USAGE_H, matrix, colChart("sizes", "Effort", rightW + 2 * avgGrow - 4, stats.sizes.map((s) => ({ label: s.size, count: s.count, tone: "idle" })), [], `estimated: ${effort.estimated} of ${effort.tasks} tasks`, USAGE_H, true), 2 * avgGrow);
794
+ const usages = pair("usage", USAGE_H, matrix, statsColChart(dim, "sizes", "Effort", rightW + 2 * avgGrow - 4, stats.sizes.map((s) => ({ label: s.size, count: s.count, tone: "idle" })), [], `estimated: ${effort.estimated} of ${effort.tasks} tasks`, USAGE_H, true), 2 * avgGrow);
786
795
  /* the table of the tasks of the highest usage: the id (inverse behind "≡", in the color of
787
796
  its lane), the title (cut with an ellipsis), a bar of its share of the total costs (resp.
788
797
  tokens), the costs, and the input and output tokens (as "<input> ▷ <output>", with
@@ -803,7 +812,7 @@ export const renderStats = (ctx) => {
803
812
  stats.top.length === 0 ? [h(Text, { key: "none", ...cx("dim", dim && "dimmed") }, "no usage")] :
804
813
  [topHead, ...stats.top.map((t, i) => {
805
814
  const id = fit(t.id, idW - 4).trimEnd();
806
- return h(Text, { key: `top${i}`, ...cx("normal", "truncate", dim && "dimmed") }, h(Text, { key: "id", color: tint(t.tone), dimColor: dim, ...cx("badge") }, ` ≡ ${id} `), " ".repeat(idW - id.length - 4) + gap + fit(clean(t.title), titleW) + gap, h(Text, { key: "bar", color: palette.accent, dimColor: dim }, statsBar(t.share, 1, pctW)), gap + statsCost(t.cost).padStart(costW) + gap +
815
+ return h(Text, { key: `top${i}`, ...cx("normal", "truncate", dim && "dimmed") }, h(Text, { key: "id", color: statsTint(t.tone), dimColor: dim, ...cx("badge") }, ` ≡ ${id} `), " ".repeat(idW - id.length - 4) + gap + fit(clean(t.title), titleW) + gap, h(Text, { key: "bar", color: palette.accent, dimColor: dim }, statsBar(t.share, 1, pctW)), gap + statsCost(t.cost).padStart(costW) + gap +
807
816
  `${statsTokens(t.input).padStart(inW)} ▷ ${statsTokens(t.output).padStart(outW)}`.padStart(tokW));
808
817
  })];
809
818
  /* the averages and lead times of the finished tasks and the efficiency of the AI usage (the
@@ -839,8 +848,8 @@ export const renderStats = (ctx) => {
839
848
  const shown = stats.states.reduce((n, s) => n + s.count, 0);
840
849
  return [
841
850
  h(Box, { key: "stats", height: boardH, paddingX: 1, flexDirection: "column", overflow: "hidden" }, ranges, chart, progress, usages, tops),
842
- h(Box, { key: "info", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), ["statistics of ", h(Text, { key: "n", ...cx("bold") }, String(shown)),
843
- ` ${filter !== "" ? "filtered " : ""}tasks`, ...(range !== "total" ? [" ", h(Text, { key: "r", ...cx("bold") }, statsRangeLabels[range])] : [])])),
851
+ h(Box, { key: "info", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), keysHint(ctx.surface, ["statistics of ", h(Text, { key: "n", ...cx("bold") }, String(shown)),
852
+ ` ${filter !== "" ? "filtered " : ""}tasks`, ...(range !== "total" ? [" ", h(Text, { key: "r", ...cx("bold") }, statsRangeLabels[range])] : [])]))),
844
853
  h(Box, { key: "keys1", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), `←/→: select time range · /: filter tasks · v/V: ${viewCycleHint("stats", agents !== null)}`)),
845
854
  h(Box, { key: "keys2", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), `M: ${mouse ? "disable" : "enable"} mouse · ?: hide key hints · q: quit`))
846
855
  ];