@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
@@ -19,8 +19,8 @@ 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
24
  /* append to the content of an info line (" " if empty) the hint how to show the
25
25
  hidden key hints again, so it need not be remembered while the key hints are hidden */
26
26
  const keysHint = (surface, info) => surface.keys ? info : info === " " ? "?: show key hints" :
@@ -39,8 +39,7 @@ export const fitGroups = (groups, collapsed, first, width) => {
39
39
  widths.push(w);
40
40
  used += gap + w;
41
41
  }
42
- const expanded = widths.map((w, i) => w === GROUP_COLL && collapsed.includes(groups[first + i].title) ? -1 : i)
43
- .filter((i) => i >= 0);
42
+ const expanded = [...widths.keys()].filter((i) => !collapsed.includes(groups[first + i].title));
44
43
  if (expanded.length > 0) {
45
44
  const extra = Math.max(0, width - used);
46
45
  const each = Math.floor(extra / expanded.length);
@@ -535,22 +534,22 @@ export const renderAgents = (ctx) => {
535
534
  const thumbH = Math.max(1, Math.round(logH * Math.min(1, logH / Math.max(1, logLines.length))));
536
535
  const thumbY = logMax === 0 ? 0 : Math.round((logH - thumbH) * logFirst / logMax);
537
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)));
538
- 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));
539
538
  const op = h(Box, { key: "op", height: agentOpHeight(agentOp), flexShrink: 0 }, pane("operation", "Operation", () => [
540
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) :
541
540
  agentOp.desc.length === 0 ? h(Text, cx("dim"), "(no operation title and description)") : ""),
542
541
  ...agentOp.desc.map((line, i) => h(Text, { key: `desc-${i}`, ...cx("normal", "truncate", dim && "dimmed") }, line))
543
542
  ], true));
544
- const info = h(Box, { key: "info", height: AGENT_INFO_H, flexShrink: 0 }, pane("process", "Process", () => [
545
- field("host:", clean(agent.host), 6),
546
- field("pid:", String(agent.pid), 6),
547
- field("uid:", clean(agentUser(agent)), 6),
548
- field("cwd:", clean(agent.cwd ?? "n/a"), 6, "truncate-start")
549
- ], true, true), pane("accounting", "Accounting", () => [
550
- field("uptime:", agentUptime(agent), 18),
551
- field("heartbeat:", `${agentDuration(now - agent.beat)} ago`, 18),
552
- field("usage (total):", agentUsage(agent.usage?.total), 18),
553
- 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)
554
553
  ], false, true));
555
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)] :
556
555
  lines.length === 0 ? [h(Text, { key: "none", ...cx("dim", dim && "dimmed") }, "(no processing log lines yet)")] :
@@ -570,8 +569,8 @@ export const renderAgents = (ctx) => {
570
569
  ];
571
570
  };
572
571
  /* the heights of the buttons of the time ranges (with a blank line below them), and of the
573
- frames "Task Progress" (and "Open Tasks" besides it), "Usage" (and "Effort" besides
574
- 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
575
574
  with an inner blank line at its top and bottom (except for the column charts), of the
576
575
  statistics view */
577
576
  const RANGES_H = 2;
@@ -619,15 +618,60 @@ const statsBar = (value, max, width) => {
619
618
  const n = max > 0 ? Math.round(value / max * width) : 0;
620
619
  return "█".repeat(n).padEnd(width, "░");
621
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
+ };
622
666
  /* render the statistics view: the framed column chart "Tasks per State" of the numbers of tasks per
623
667
  lifecycle state (labeled by state, its effort, and phase, in the colors of their lanes, with the framed column
624
- 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
625
669
  "Task Progress" of the cancelled, finished, work-in-progress, and open tasks (with the framed
626
- 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"
627
671
  of the total times, tokens, and costs of the tasks (with the framed column chart "Effort"
628
- 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
629
673
  tasks of the highest usage (with the framed "Finished Tasks" of the usage per finished task and
630
- 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 */
631
675
  export const renderStats = (ctx) => {
632
676
  const { stats, usage, range, rangeRef, columns, boardH, dim, mouse, filter, agents } = ctx;
633
677
  const innerW = Math.max(20, columns - 6);
@@ -640,55 +684,15 @@ export const renderStats = (ctx) => {
640
684
  /* the number of columns the frames "Finished Tasks" and "Efficiency" are each grown by
641
685
  (at the expense of the frame "Top-5 Tasks") */
642
686
  const avgGrow = 4;
643
- const tint = (tone) => tone === "active" ? palette.accent : tone === "done" ? palette.dim : palette.normal;
644
- 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), " ]")));
645
- const frame = frameOf(false);
646
- const accentFrame = frameOf(true);
687
+ const frame = statsFrame(dim, false);
688
+ const accentFrame = statsFrame(dim, true);
647
689
  if (stats === null)
648
690
  return [h(Box, { key: "stats", height: boardH }), h(Text, { key: "info" }, " "), h(Text, { key: "keys1" }, " "), h(Text, { key: "keys2" }, " ")];
649
- /* a framed column chart of an inner width, where the highest column leaves the top row
650
- for its number, each number sits on top of its column (being at least a thin line,
651
- even without any number, like in the web board), the columns stand on a blank line above
652
- their labels (each cut to its first five characters, without an ellipsis, for
653
- readability) and their sub-labels below them (like the effort of a state, cut with
654
- an ellipsis, and blank if empty, to keep the columns of all charts aligned), and each
655
- group of columns is labeled below its line spanning its columns
656
- (like in the web board), followed by a footer line (blank if empty, to keep the columns
657
- of all charts aligned), where the columns start directly at the left padding of the
658
- frame, are separated by a gap of one column, and leave the remaining width on the right,
659
- 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 */
660
692
  const chartH = Math.max(9, boardH - RANGES_H - PROGRESS_H - USAGE_H - TOP_H);
661
- const colChart = (key, title, width, cols, groups, footer = "", height = chartH, compact = false) => {
662
- const areaH = Math.max(1, height - (compact ? 4 : 8));
663
- const colW = Math.max(2, Math.floor((width + 1) / cols.length));
664
- const lineW = colW - 1;
665
- const fitW = Math.max(1, Math.min(14, lineW));
666
- const barW = fitW > 1 && (lineW - fitW) % 2 !== 0 ? fitW - 1 : fitW;
667
- const barL = Math.floor((lineW - barW) / 2);
668
- const max = Math.max(1, ...cols.map((c) => c.count));
669
- const gapOf = (i, n) => i < n - 1 ? " " : "";
670
- 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));
671
- const rows = Array.from({ length: areaH }, (_x, r) => {
672
- const level = areaH - 1 - r;
673
- return h(Box, { key: `row${r}` }, h(Text, { wrap: "truncate" }, ...cols.map((c, i) => {
674
- const fill = Math.max(1, Math.round(c.count / max * (areaH - 1) * 8));
675
- const cell = Math.max(0, Math.min(8, fill - level * 8));
676
- const text = String(c.count);
677
- if (cell === 0 && level === Math.ceil(fill / 8) && text.length > barW)
678
- return h(Text, { key: i, color: tint(c.tone), dimColor: dim }, center(text, lineW) + gapOf(i, cols.length));
679
- return column(i, c.tone, cell > 0 ? lowerEighths[cell].repeat(barW) :
680
- level === Math.ceil(fill / 8) ? center(text, barW) : " ".repeat(barW));
681
- })));
682
- });
683
- 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)))));
684
- const foot = h(Box, { key: "footer" }, h(Text, { ...cx("dim", dim && "dimmed"), wrap: "truncate" }, center(footer, Math.max(0, cols.length * colW - 1))));
685
- if (compact)
686
- return frame(key, height, title, 0, ...rows, labels, foot);
687
- 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);
688
- };
689
693
  /* the column chart of the number of tasks per state (labeled by the states and below by the
690
694
  effort of their estimated tasks, grouped by their phases, and with the total number of tasks
691
- 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
692
696
  their week numbers, prefixed with "W" if fitting, and grouped by all weeks with the total
693
697
  and average number of finished tasks, and with the forecast of the backlog as its footer),
694
698
  grown like the frames "Finished Tasks" and "Efficiency" together, to align with them */
@@ -704,10 +708,10 @@ export const renderStats = (ctx) => {
704
708
  const weekW = Math.max(2, Math.floor(weeksW / Math.max(1, stats.throughput.length))) - 1;
705
709
  const weekSum = stats.throughput.reduce((n, w) => n + w.count, 0);
706
710
  const weekAvg = stats.throughput.length > 0 ? weekSum / stats.throughput.length : 0;
707
- 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) => ({
708
712
  label: s.status, count: s.count, tone: s.tone,
709
713
  sub: s.effort.estimated > 0 ? statsHoursN(s.effort.minutes) : ""
710
- })), 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);
711
715
  /* a framed stacked bar of the numbers of tasks of parts, with its legend */
712
716
  const stacked = (key, title, width, parts) => {
713
717
  const total = parts.reduce((n, p) => n + p.count, 0);
@@ -785,9 +789,9 @@ export const renderStats = (ctx) => {
785
789
  [h(Text, { key: "loading", ...cx("dim", dim && "dimmed") }, "loading …")] : [head, ...lines]));
786
790
  /* the compact column chart of the distribution of the effort sizes (the numbers of the
787
791
  estimated tasks per size, from the smallest to the largest size), with the numbers of
788
- 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 */
789
793
  const effort = stats.states.reduce((sum, s) => ({ estimated: sum.estimated + s.effort.estimated, tasks: sum.tasks + s.effort.tasks }), { estimated: 0, tasks: 0 });
790
- 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);
791
795
  /* the table of the tasks of the highest usage: the id (inverse behind "≡", in the color of
792
796
  its lane), the title (cut with an ellipsis), a bar of its share of the total costs (resp.
793
797
  tokens), the costs, and the input and output tokens (as "<input> ▷ <output>", with
@@ -808,7 +812,7 @@ export const renderStats = (ctx) => {
808
812
  stats.top.length === 0 ? [h(Text, { key: "none", ...cx("dim", dim && "dimmed") }, "no usage")] :
809
813
  [topHead, ...stats.top.map((t, i) => {
810
814
  const id = fit(t.id, idW - 4).trimEnd();
811
- 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 +
812
816
  `${statsTokens(t.input).padStart(inW)} ▷ ${statsTokens(t.output).padStart(outW)}`.padStart(tokW));
813
817
  })];
814
818
  /* the averages and lead times of the finished tasks and the efficiency of the AI usage (the