@rse/ase 2.0.1 → 2.0.2

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 (119) hide show
  1. package/dst/ase-config.js +2 -1
  2. package/dst/ase-hook.js +2 -7
  3. package/dst/ase-lib-server-cli.js +6 -5
  4. package/dst/ase-task-board-core.js +1 -1
  5. package/dst/ase-task-board-tui-control.js +10 -10
  6. package/dst/ase-task-board-tui-model.js +10 -14
  7. package/dst/ase-task-board-tui-view.js +14 -9
  8. package/dst/ase-task-board-web-client.css +1 -1
  9. package/dst/ase-task-board-web-client.js +2 -2
  10. package/dst/ase-task-board-web-server.js +2 -25
  11. package/dst/ase-task-store-issues.js +76 -0
  12. package/dst/ase-task-store-plugin-gitea.js +2 -65
  13. package/dst/ase-task-store-plugin-github.js +2 -65
  14. package/dst/ase-task-store-plugin-gitlab.js +2 -65
  15. package/dst/ase-util-diagram.js +3 -5
  16. package/dst/man/ase-agent-adhoc.1 +1 -1
  17. package/dst/man/ase-agent-condenser.1 +1 -1
  18. package/dst/man/ase-agent-editor.1 +1 -1
  19. package/dst/man/ase-agent-estimator.1 +1 -1
  20. package/dst/man/ase-agent-fleet.1 +1 -1
  21. package/dst/man/ase-agent-implementer.1 +1 -1
  22. package/dst/man/ase-agent-integrator.1 +1 -1
  23. package/dst/man/ase-agent-qualifier.1 +1 -1
  24. package/dst/man/ase-agent-rebooter.1 +1 -1
  25. package/dst/man/ase-agent-relay.1 +1 -1
  26. package/dst/man/ase-agent-researcher.1 +1 -1
  27. package/dst/man/ase-agent.1 +1 -1
  28. package/dst/man/ase-artifact-list.1 +1 -1
  29. package/dst/man/ase-artifact-name.1 +1 -1
  30. package/dst/man/ase-artifact.1 +1 -1
  31. package/dst/man/ase-config-delete.1 +1 -1
  32. package/dst/man/ase-config-edit.1 +1 -1
  33. package/dst/man/ase-config-get.1 +1 -1
  34. package/dst/man/ase-config-init.1 +1 -1
  35. package/dst/man/ase-config-list.1 +1 -1
  36. package/dst/man/ase-config-set.1 +1 -1
  37. package/dst/man/ase-config.1 +1 -1
  38. package/dst/man/ase-hook-permission-request.1 +1 -1
  39. package/dst/man/ase-hook-pre-tool-use.1 +1 -1
  40. package/dst/man/ase-hook-session-end.1 +1 -1
  41. package/dst/man/ase-hook-session-persona.1 +1 -1
  42. package/dst/man/ase-hook-session-start.1 +1 -1
  43. package/dst/man/ase-hook-stop.1 +1 -1
  44. package/dst/man/ase-hook-user-prompt-submit.1 +1 -1
  45. package/dst/man/ase-hook.1 +1 -1
  46. package/dst/man/ase-man.1 +1 -1
  47. package/dst/man/ase-mcp.1 +1 -1
  48. package/dst/man/ase-service-log.1 +1 -1
  49. package/dst/man/ase-service-send.1 +1 -1
  50. package/dst/man/ase-service-start.1 +1 -1
  51. package/dst/man/ase-service-status.1 +1 -1
  52. package/dst/man/ase-service-stop.1 +1 -1
  53. package/dst/man/ase-service.1 +1 -1
  54. package/dst/man/ase-setup-disable.1 +1 -1
  55. package/dst/man/ase-setup-enable.1 +1 -1
  56. package/dst/man/ase-setup-install.1 +1 -1
  57. package/dst/man/ase-setup-mcp.1 +1 -1
  58. package/dst/man/ase-setup-status.1 +1 -1
  59. package/dst/man/ase-setup-statusline.1 +1 -1
  60. package/dst/man/ase-setup-telemetry.1 +1 -1
  61. package/dst/man/ase-setup-uninstall.1 +1 -1
  62. package/dst/man/ase-setup-update.1 +1 -1
  63. package/dst/man/ase-setup.1 +1 -1
  64. package/dst/man/ase-spec-export.1 +1 -1
  65. package/dst/man/ase-spec-lint.1 +1 -1
  66. package/dst/man/ase-spec-preview.1 +1 -1
  67. package/dst/man/ase-spec.1 +1 -1
  68. package/dst/man/ase-statusline.1 +1 -1
  69. package/dst/man/ase-task-assign.1 +1 -1
  70. package/dst/man/ase-task-board.1 +3 -3
  71. package/dst/man/ase-task-delete.1 +1 -1
  72. package/dst/man/ase-task-edit.1 +1 -1
  73. package/dst/man/ase-task-idscheme.1 +1 -1
  74. package/dst/man/ase-task-lifecycle.1 +1 -1
  75. package/dst/man/ase-task-list.1 +1 -1
  76. package/dst/man/ase-task-load.1 +1 -1
  77. package/dst/man/ase-task-newid.1 +1 -1
  78. package/dst/man/ase-task-purge.1 +1 -1
  79. package/dst/man/ase-task-rename.1 +1 -1
  80. package/dst/man/ase-task-request.1 +1 -1
  81. package/dst/man/ase-task-save.1 +1 -1
  82. package/dst/man/ase-task-status.1 +1 -1
  83. package/dst/man/ase-task-store.1 +1 -1
  84. package/dst/man/ase-task-view.1 +1 -1
  85. package/dst/man/ase-task.1 +1 -1
  86. package/dst/man/ase-util-compat.1 +1 -1
  87. package/dst/man/ase-util-diagram.1 +1 -1
  88. package/dst/man/ase-util-meta.1 +1 -1
  89. package/dst/man/ase-util-metric.1 +1 -1
  90. package/dst/man/ase-util-mint.1 +1 -1
  91. package/dst/man/ase-util-worktree.1 +1 -1
  92. package/dst/man/ase-util.1 +1 -1
  93. package/dst/man/ase.1 +1 -1
  94. package/package.json +1 -1
  95. package/plugin/.claude-plugin/plugin.json +1 -1
  96. package/plugin/.codex-plugin/plugin.json +1 -1
  97. package/plugin/.github/plugin/plugin.json +1 -1
  98. package/plugin/meta/ase-common-resolve.md +2 -2
  99. package/plugin/mods/ase-mods.ts +6 -5
  100. package/plugin/skills/ase-arch-analyze/SKILL.md +3 -1
  101. package/plugin/skills/ase-code-analyze/SKILL.md +8 -0
  102. package/plugin/skills/ase-code-edit/SKILL.md +5 -4
  103. package/plugin/skills/ase-code-edit/help.md +1 -1
  104. package/plugin/skills/ase-code-lint/SKILL.md +15 -4
  105. package/plugin/skills/ase-help-intent/data.md +36 -18
  106. package/plugin/skills/ase-help-skill/help.md +3 -4
  107. package/plugin/skills/ase-meta-quorum/SKILL.md +9 -0
  108. package/plugin/skills/ase-meta-quorum/help.md +5 -3
  109. package/plugin/skills/ase-meta-workflow/sample.md +5 -3
  110. package/plugin/skills/ase-repo-changelog/SKILL.md +8 -9
  111. package/plugin/skills/ase-repo-changelog/help.md +2 -2
  112. package/plugin/skills/ase-spec-edit/SKILL.md +2 -1
  113. package/plugin/skills/ase-spec-edit/help.md +2 -1
  114. package/plugin/skills/ase-sync-reconcile/help.md +4 -4
  115. package/plugin/skills/ase-task-dissect/SKILL.md +10 -6
  116. package/plugin/skills/ase-task-grill/help.md +1 -1
  117. package/plugin/skills/ase-task-implement/SKILL.md +58 -1
  118. package/plugin/skills/ase-task-implement/help.md +16 -0
  119. package/plugin/skills/ase-task-list/help.md +2 -2
package/dst/ase-config.js CHANGED
@@ -191,7 +191,8 @@ export class ConfigMCP {
191
191
  const cfg = new Config("config", configSchema, this.log, scope);
192
192
  cfg.read();
193
193
  const val = cfg.get(args.key);
194
- const text = val === undefined ? "" : JSON.stringify(val);
194
+ const text = val === undefined ? "" :
195
+ JSON.stringify(configSecretKeys.includes(cfg.resolveKey(args.key)) ? "***" : val);
195
196
  return { content: [{ type: "text", text }] };
196
197
  }
197
198
  catch (err) {
package/dst/ase-hook.js CHANGED
@@ -16,6 +16,7 @@ import { parseScope, userStateDir } from "./ase-config-scope.js";
16
16
  import { readStdin, writeStdout } from "./ase-lib-stdio.js";
17
17
  import { Task } from "./ase-task.js";
18
18
  import * as TaskFormat from "./ase-task-format.js";
19
+ import { parseTool } from "./ase-setup-common.js";
19
20
  const addonMcpServers = [
20
21
  "chat-alibaba-qwen",
21
22
  "chat-deepseek",
@@ -706,12 +707,6 @@ export default class HookCommand {
706
707
  }
707
708
  return 0;
708
709
  }
709
- /* parse and validate the --tool option */
710
- parseTool(value) {
711
- if (value !== "claude" && value !== "copilot" && value !== "codex")
712
- throw new Error(`invalid --tool value: "${value}" (expected "claude", "copilot", or "codex")`);
713
- return value;
714
- }
715
710
  /* register commands */
716
711
  register(program) {
717
712
  /* default for --tool derived from ASE_TOOL environment variable
@@ -743,7 +738,7 @@ export default class HookCommand {
743
738
  .description(desc)
744
739
  .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
745
740
  .action(async (opts) => {
746
- process.exitCode = await handler(this.parseTool(opts.tool));
741
+ process.exitCode = await handler(parseTool(opts.tool));
747
742
  });
748
743
  }
749
744
  }
@@ -13,6 +13,7 @@ import { isSeq, isScalar } from "yaml";
13
13
  import { Config } from "./ase-config-core.js";
14
14
  import { parseScope } from "./ase-config-scope.js";
15
15
  import { isLogLevel } from "./ase-lib-log.js";
16
+ import { writeStdout } from "./ase-lib-stdio.js";
16
17
  import { Service } from "./ase-service.js";
17
18
  import { DaemonCommand } from "./ase-lib-daemon-cli.js";
18
19
  import { urlHost, isLoopback, serverHeader } from "./ase-lib-server-bind.js";
@@ -381,26 +382,26 @@ export class ServerCommand extends DaemonCommand {
381
382
  async doStatus() {
382
383
  const ctx = this.loadContext();
383
384
  if (ctx.port === null) {
384
- process.stdout.write(`${this.label}: not running (no port configured)\n`);
385
+ await writeStdout(`${this.label}: not running (no port configured)\n`);
385
386
  return 1;
386
387
  }
387
388
  const address = ctx.address ?? DEFAULT_ADDRESS;
388
389
  const scheme = ctx.tlsCert !== null ? "https" : "http";
389
390
  const state = await this.probe(address, ctx.port, ctx.token, ctx.tlsCert !== null);
390
391
  if (state === "running") {
391
- process.stdout.write(`${this.label}: running on ${scheme}://${urlHost(address)}:${ctx.port}` +
392
+ await writeStdout(`${this.label}: running on ${scheme}://${urlHost(address)}:${ctx.port}` +
392
393
  (ctx.pid !== null ? ` (pid ${ctx.pid})` : "") + this.running(ctx) + "\n");
393
394
  return 0;
394
395
  }
395
396
  if (state === "unauthorized") {
396
- process.stdout.write(`${this.label}: running on ${scheme}://${urlHost(address)}:${ctx.port}, but the configured token is rejected\n`);
397
+ await writeStdout(`${this.label}: running on ${scheme}://${urlHost(address)}:${ctx.port}, but the configured token is rejected\n`);
397
398
  return 1;
398
399
  }
399
400
  if (state === "foreign") {
400
- process.stdout.write(`${this.label}: not running (port ${ctx.port} in use by foreign service)\n`);
401
+ await writeStdout(`${this.label}: not running (port ${ctx.port} in use by foreign service)\n`);
401
402
  return 1;
402
403
  }
403
- process.stdout.write(`${this.label}: not running (port ${ctx.port} not responding)\n`);
404
+ await writeStdout(`${this.label}: not running (port ${ctx.port} not responding)\n`);
404
405
  return 1;
405
406
  }
406
407
  /* stop flow: terminate the server process and wait until it is gone */
@@ -187,7 +187,7 @@ export class BoardState {
187
187
  state[surface][flag] = !state[surface][flag];
188
188
  });
189
189
  }
190
- /* set the shown view (lanes, graph, stats, or agents) */
190
+ /* set the shown view (lanes, graph, stats, agents, or flow) */
191
191
  static setView(surface, view) {
192
192
  return BoardState.update((state) => {
193
193
  state[surface].view = view;
@@ -310,16 +310,6 @@ const handlePopupKey = (ctx, input, key) => {
310
310
  setPrompt({ ...prompt, focus: prompt.focus === "request" ? "cancel" : "request" });
311
311
  return;
312
312
  }
313
- /* type the prompt of an edit request: RETURN requests the edit, Shift+RETURN
314
- (under the kitty keyboard protocol only), Alt+RETURN (under the kitty keyboard
315
- protocol, or where the terminal sends it as ESC+RETURN, e.g. with "Option as Meta"
316
- on macOS), and Ctrl+J (in every terminal, as a plain linefeed without the kitty
317
- keyboard protocol) insert a newline,
318
- the left/right arrows move the cursor by one character, HOME/END move it
319
- to the start/end, BACKSPACE removes the character before the cursor, DELETE
320
- removes the character at the cursor, and all other input (also pasted text,
321
- with its newlines kept and its other control characters turned into spaces)
322
- is inserted at the cursor */
323
313
  if (prompt !== null && prompt.request !== null && prompt.focus === "name") {
324
314
  /* type the request name of a request by prompt: RETURN switches to the prompt (resp. to
325
315
  the request button if the prompt is ignored), the
@@ -342,6 +332,16 @@ const handlePopupKey = (ctx, input, key) => {
342
332
  name(request.name + input.toLowerCase());
343
333
  return;
344
334
  }
335
+ /* type the prompt of an edit request: RETURN requests the edit, Shift+RETURN
336
+ (under the kitty keyboard protocol only), Alt+RETURN (under the kitty keyboard
337
+ protocol, or where the terminal sends it as ESC+RETURN, e.g. with "Option as Meta"
338
+ on macOS), and Ctrl+J (in every terminal, as a plain linefeed without the kitty
339
+ keyboard protocol) insert a newline,
340
+ the left/right arrows move the cursor by one character, HOME/END move it
341
+ to the start/end, BACKSPACE removes the character before the cursor, DELETE
342
+ removes the character at the cursor, and all other input (also pasted text,
343
+ with its newlines kept and its other control characters turned into spaces)
344
+ is inserted at the cursor */
345
345
  if (prompt !== null) {
346
346
  const chars = [...prompt.text];
347
347
  const pos = Math.max(0, Math.min(prompt.pos, chars.length));
@@ -161,6 +161,14 @@ const boxAt = (map, mx, my) => {
161
161
  }
162
162
  return undefined;
163
163
  };
164
+ /* the box of every node of a laid out graph, for the spatial navigation and the scrolling */
165
+ const placesOf = (graph) => {
166
+ const map = new Map();
167
+ if (graph !== undefined)
168
+ for (const n of graph.nodes.values())
169
+ map.set(n.id, { r: n.y + Math.floor(n.h / 2), c: n.x + Math.floor(n.w / 2), top: n.y, bottom: n.y + n.h - 1, bl: n.x, br: n.x + n.w - 1 });
170
+ return map;
171
+ };
164
172
  /* the state of the agents view: the agents of the master list, the selected one (followed by its id,
165
173
  else, until explicitly selected again, the one at the index of the previously selected one),
166
174
  the lines of its operation pane, and the lines of its recent processing log in the detail view (within its frame), scrolled
@@ -343,13 +351,7 @@ export const useBoardState = (log, initial, relay) => {
343
351
  }
344
352
  }, [all, prompt]);
345
353
  /* remember the box of every graph node for the spatial navigation and the scrolling */
346
- const places = React.useMemo(() => {
347
- const map = new Map();
348
- if (layout !== null)
349
- for (const n of layout.graph.nodes.values())
350
- map.set(n.id, { r: n.y + Math.floor(n.h / 2), c: n.x + Math.floor(n.w / 2), top: n.y, bottom: n.y + n.h - 1, bl: n.x, br: n.x + n.w - 1 });
351
- return map;
352
- }, [layout]);
354
+ const places = React.useMemo(() => placesOf(layout?.graph), [layout]);
353
355
  /* follow changes of the task storage and of the lifecycle mode,
354
356
  and the kind and connection state of the task store
355
357
  (in the background, i.e. without the busy popup) */
@@ -416,13 +418,7 @@ export const useBoardState = (log, initial, relay) => {
416
418
  if (!flow.cards.has(flowSel) && !flow.states.some((s) => flowState(s) === flowSel) && flow.cards.size > 0)
417
419
  setFlowSel(flow.cards.keys().next().value);
418
420
  }, [flow, flowSel]);
419
- const flowPlaces = React.useMemo(() => {
420
- const map = new Map();
421
- if (flowLayout !== null)
422
- for (const n of flowLayout.graph.nodes.values())
423
- map.set(n.id, { r: n.y + Math.floor(n.h / 2), c: n.x + Math.floor(n.w / 2), top: n.y, bottom: n.y + n.h - 1, bl: n.x, br: n.x + n.w - 1 });
424
- return map;
425
- }, [flowLayout]);
421
+ const flowPlaces = React.useMemo(() => placesOf(flowLayout?.graph), [flowLayout]);
426
422
  /* fetch the AI usage of all tasks whenever the statistics view is shown or the board
427
423
  changes, but only while the statistics view is shown (as each fetching loads all
428
424
  plans), and once known in the background, i.e. without the busy popup */
@@ -21,6 +21,10 @@ const GRID_MIN = 30;
21
21
  const GRID_GAP = 1;
22
22
  /* make a text of the agent relay safe for the terminal (strip control characters) */
23
23
  const clean = (text) => text.replace(/\p{Cc}/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) */
@@ -331,7 +335,8 @@ export const renderLanes = (ctx) => {
331
335
  const arrows = shown < total && !grown;
332
336
  const group = board.groups[sel.g];
333
337
  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"}`;
338
+ `groups ${fit.first + 1}–${fit.last + 1} of ${total} · ${arrows ? "←/→ scrolls" : "all visible"}` +
339
+ (surface.keys ? "" : " · ?: show key hints");
335
340
  const view = { board, surface, sel, dim, carry, titles: surface.titles, scroll: { left: -1, right: -1 }, pulse, cardRef, laneRef, groupRef, agents, working };
336
341
  /* the scroll bar consumes the entire width remaining besides the info */
337
342
  const track = Math.max(1, columns - 2 - info.length - 3);
@@ -390,8 +395,8 @@ export const renderGraph = (ctx) => {
390
395
  })))))),
391
396
  /* overlay the graph statistics centered onto the top border */
392
397
  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)])),
398
+ h(Box, { key: "info", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), keysHint(surface, card === undefined ? " " :
399
+ ["task: ", h(Text, { key: "id", ...cx("bold") }, card.id), " · status: ", h(Text, { key: "status", ...cx("bold") }, card.status)]))),
395
400
  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
401
  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
402
  `s: ${surface.standalone ? "hide" : "show"} standalone tasks`)),
@@ -434,7 +439,7 @@ export const renderFlow = (ctx) => {
434
439
  })))))),
435
440
  /* overlay the graph statistics centered onto the top border */
436
441
  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 ? [
442
+ h(Box, { key: "info", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), keysHint(ctx.surface, rel !== null ? [
438
443
  "state: ", h(Text, { key: "state", ...cx("bold") }, rel.state),
439
444
  " · observed by: ", h(Text, { key: "in", ...cx("bold") }, names(rel.input)),
440
445
  " · written by: ", h(Text, { key: "out", ...cx("bold") }, names(rel.output)),
@@ -448,7 +453,7 @@ export const renderFlow = (ctx) => {
448
453
  " · output: ", h(Text, { key: "out", ...cx("bold") }, states.output.join(", ") || "none"),
449
454
  ...(states.gate !== undefined ? [" · gate: ", h(Text, { key: "gate", ...cx("bold") }, states.gate)] : [])
450
455
  ])
451
- ])),
456
+ ]))),
452
457
  h(Box, { key: "keys1", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), "↑/↓/←/→/Left-Click: select agent/state · ⏎/Double-Click: view agent · " +
453
458
  "⇈/⇊/⇤/⇥/Wheel: scroll canvas")),
454
459
  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`))
@@ -554,12 +559,12 @@ export const renderAgents = (ctx) => {
554
559
  const detail = h(Box, { key: "detail", flexGrow: 1, height: boardH, marginLeft: 1, flexDirection: "column" }, op, info, log);
555
560
  return [
556
561
  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 ? " " : [
562
+ h(Box, { key: "info", ...cx("bar") }, h(Text, cx("hint", "truncate", dim && "dimmed"), keysHint(ctx.surface, agent === undefined ? " " : [
558
563
  "agent: ", h(Text, { key: "agent", ...cx("bold") }, clean(agentLabel(agent))),
559
564
  ...(agentOp.title !== "" ? [
560
565
  " · operation: ", h(Text, { key: "op", ...cx("bold") }, agentOp.title)
561
566
  ] : [])
562
- ])),
567
+ ]))),
563
568
  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
569
  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
570
  ];
@@ -839,8 +844,8 @@ export const renderStats = (ctx) => {
839
844
  const shown = stats.states.reduce((n, s) => n + s.count, 0);
840
845
  return [
841
846
  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])] : [])])),
847
+ 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)),
848
+ ` ${filter !== "" ? "filtered " : ""}tasks`, ...(range !== "total" ? [" ", h(Text, { key: "r", ...cx("bold") }, statsRangeLabels[range])] : [])]))),
844
849
  h(Box, { key: "keys1", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), `←/→: select time range · /: filter tasks · v/V: ${viewCycleHint("stats", agents !== null)}`)),
845
850
  h(Box, { key: "keys2", ...cx("bar") }, h(Text, cx("hint", dim && "dimmed"), `M: ${mouse ? "disable" : "enable"} mouse · ?: hide key hints · q: quit`))
846
851
  ];