@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
@@ -14,10 +14,11 @@ import { Config, sessionState } from "./ase-config-core.js";
14
14
  import { configSchema } from "./ase-config-schema.js";
15
15
  import { parseScope, userStateDir, projectIdOf } from "./ase-config-scope.js";
16
16
  import { readStdin, writeStdout } from "./ase-lib-stdio.js";
17
+ import { parseTool } from "./ase-setup-common.js";
17
18
  import pkg from "../package.json" with { type: "json" };
18
19
  /* forced-color chalk instance: stdout is a pipe under Anthropic Claude Code CLI,
19
20
  so chalk auto-detection would yield level 0; force level 1 to keep
20
- emitting ANSI sequences as the original implementation did */
21
+ emitting ANSI sequences */
21
22
  const c = new Chalk({ level: 1 });
22
23
  /* set of valid <color>...</color> markup names (chalk basic foreground colors plus "default") */
23
24
  const COLORS = new Set([
@@ -25,10 +26,9 @@ const COLORS = new Set([
25
26
  ]);
26
27
  /* custom argument parser for Commander: non-negative integer */
27
28
  const parseInteger = (name) => (value) => {
28
- const n = Number.parseInt(value, 10);
29
- if (!Number.isFinite(n) || n < 0)
29
+ if (!/^\d+$/.test(value))
30
30
  throw new InvalidArgumentError(`${name} must be a non-negative integer`);
31
- return n;
31
+ return Number.parseInt(value, 10);
32
32
  };
33
33
  /* detect terminal column width via /dev/tty (stdout is a pipe under Anthropic Claude Code CLI) */
34
34
  const detectTermWidth = () => {
@@ -39,7 +39,7 @@ const detectTermWidth = () => {
39
39
  const out = execFileSync("tput", ["cols"], { stdio: [tty, "pipe", "ignore"] });
40
40
  width = Number.parseInt(out.toString("utf8").trim(), 10) || 0;
41
41
  }
42
- catch (_e) {
42
+ catch {
43
43
  /* no controlling terminal */
44
44
  }
45
45
  finally {
@@ -47,7 +47,7 @@ const detectTermWidth = () => {
47
47
  try {
48
48
  fs.closeSync(tty);
49
49
  }
50
- catch (_e) {
50
+ catch {
51
51
  /* best-effort */
52
52
  }
53
53
  }
@@ -141,12 +141,12 @@ const probeGit = (cwd) => {
141
141
  if (mDel !== null)
142
142
  removed = Number.parseInt(mDel[1], 10);
143
143
  }
144
- catch (_e) {
144
+ catch {
145
145
  /* no HEAD yet or git failure; leave counts at 0 */
146
146
  }
147
147
  return { branch, untracked, added, removed };
148
148
  }
149
- catch (_e) {
149
+ catch {
150
150
  return { branch: "", untracked: 0, added: 0, removed: 0 };
151
151
  }
152
152
  };
@@ -157,7 +157,7 @@ const probeMemory = () => {
157
157
  const free = os.freemem();
158
158
  return { used: total - free, total };
159
159
  }
160
- catch (_e) {
160
+ catch {
161
161
  return { used: 0, total: 0 };
162
162
  }
163
163
  };
@@ -197,7 +197,7 @@ const fetchCodexLimits = async () => {
197
197
  try {
198
198
  cache = (JSON.parse(fs.readFileSync(cacheFile, "utf8")) ?? {});
199
199
  }
200
- catch (_e) {
200
+ catch {
201
201
  /* no (valid) cache yet */
202
202
  }
203
203
  if (typeof cache.time === "number" && Math.abs(Date.now() - cache.time) < CODEX_USAGE_TTL)
@@ -205,7 +205,8 @@ const fetchCodexLimits = async () => {
205
205
  /* query the usage endpoint (keeping the stale result on any failure) */
206
206
  let limits = cache.limits;
207
207
  try {
208
- const home = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
208
+ const dir = process.env.CODEX_HOME ?? "";
209
+ const home = dir !== "" ? dir : path.join(os.homedir(), ".codex");
209
210
  const auth = JSON.parse(fs.readFileSync(path.join(home, "auth.json"), "utf8"));
210
211
  const token = auth?.tokens?.access_token;
211
212
  if (typeof token === "string" && token !== "") {
@@ -233,7 +234,7 @@ const fetchCodexLimits = async () => {
233
234
  }
234
235
  }
235
236
  }
236
- catch (_e) {
237
+ catch {
237
238
  /* credential unreadable or endpoint unreachable */
238
239
  }
239
240
  /* update the cache */
@@ -241,23 +242,166 @@ const fetchCodexLimits = async () => {
241
242
  fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
242
243
  fs.writeFileSync(cacheFile, JSON.stringify({ time: Date.now(), limits }) + "\n", "utf8");
243
244
  }
244
- catch (_e) {
245
+ catch {
245
246
  /* best-effort */
246
247
  }
247
248
  return limits;
248
249
  };
250
+ /* create the identifier to renderer map: each callback fetches its own information
251
+ directly from data (or via the lazy probes for shared values) */
252
+ const createRenderers = (ctx) => {
253
+ const { data, tool, limits, emit, prefix, getSession, getCfg, getModel, getGit, getMem } = ctx;
254
+ /* render a configuration value (if non-empty) with its icon and label */
255
+ const cfgValue = (key, icon, label) => () => {
256
+ const value = getCfg()[key];
257
+ if (value !== "")
258
+ emit(`${prefix(icon, label)}${c.bold(value)}`);
259
+ };
260
+ return {
261
+ /* ==== SCOPE ==== */
262
+ u: () => {
263
+ const user = process.env.USER ?? process.env.LOGNAME ?? "unknown";
264
+ emit(`${prefix("※", "user")}${c.bold(user)}`);
265
+ },
266
+ p: () => {
267
+ const id = projectIdOf(data.workspace?.current_dir ?? "");
268
+ emit(`${prefix("⚑", "project")}${c.bold(id)}`);
269
+ },
270
+ T: cfgValue("taskId", "◉", "task"),
271
+ s: () => emit(`${prefix("⏻", "session")}${c.bold(getSession())}`),
272
+ /* ==== MODEL ==== */
273
+ m: () => {
274
+ const { name } = getModel();
275
+ emit(`${prefix("⚙", "model")}${c.bold(name)}`);
276
+ },
277
+ e: () => {
278
+ const { effort } = getModel();
279
+ emit(`${prefix("⚒", "effort")}${c.bold(effort !== "" ? effort : "unknown")}`);
280
+ },
281
+ t: () => {
282
+ const thinking = data.thinking?.enabled === true ? "yes" : "no";
283
+ emit(`${prefix("⚛", "thinking")}${c.bold(thinking)}`);
284
+ },
285
+ /* ==== OUTPUT ==== */
286
+ O: () => {
287
+ const styleName = data.output_style?.name ?? "";
288
+ if (styleName !== "")
289
+ emit(`${prefix("≡", "style")}${c.bold(styleName)}`);
290
+ },
291
+ P: cfgValue("persona", "☯", "persona"),
292
+ h: cfgValue("guidance", "▶", "guidance"),
293
+ B: cfgValue("boxing", "▢", "boxing"),
294
+ L: cfgValue("lifecycle", "⟳", "lifecycle"),
295
+ /* ==== CONTEXT ==== */
296
+ c: () => {
297
+ const pct = Math.min(100, Math.max(0, Math.floor(data.context_window?.used_percentage ?? 0)));
298
+ const barSize = 20;
299
+ const filled = Math.round(pct / 100 * barSize);
300
+ const bar = "█".repeat(filled) + "░".repeat(barSize - filled);
301
+ emit(`${prefix("◔", "context")}${bar} ${pct}%`);
302
+ },
303
+ C: () => {
304
+ /* probe the live-context fields first and fall back to the
305
+ cumulative ones, so each harness contributes its own view:
306
+ GitHub Copilot CLI reports the live window separately (its
307
+ "total_*" are session sums), whereas Anthropic Claude Code CLI
308
+ folds it into "total_input_tokens". Output tokens stay out --
309
+ "used_percentage" is input-side only. */
310
+ const cw = data.context_window;
311
+ const pct = cw?.used_percentage ?? 0;
312
+ const tokensCur = cw?.current_context_tokens ??
313
+ cw?.last_call_input_tokens ?? cw?.total_input_tokens ?? 0;
314
+ const tokensLim = cw?.displayed_context_limit ?? cw?.context_window_size ??
315
+ (pct > 0 && tokensCur > 0 ? Math.round(tokensCur * 100 / pct) : 0);
316
+ if (tokensLim > 0)
317
+ emit(`${prefix("◆", "tokens")}${c.bold(formatTokens(tokensCur) + "/" + formatTokens(tokensLim))}`);
318
+ },
319
+ /* ==== RATE LIMITS ==== */
320
+ S: () => {
321
+ const pct5h = limits?.five_hour?.used_percentage;
322
+ const s = pct5h !== undefined ? `${pct5h.toFixed(1)}%` : "-";
323
+ emit(`${prefix("⏲", "5h-usage")}${c.bold(s)}`);
324
+ },
325
+ D: () => {
326
+ const until5h = limits?.five_hour?.resets_at;
327
+ const s = formatTimeUntil(until5h);
328
+ emit(`${prefix("⏱", "5h-resets")}${c.bold(s !== "" ? s : "-")}`);
329
+ },
330
+ W: () => {
331
+ const pctWk = limits?.seven_day?.used_percentage;
332
+ const s = pctWk !== undefined ? `${pctWk.toFixed(1)}%` : "-";
333
+ emit(`${prefix("⏲", "7d-usage")}${c.bold(s)}`);
334
+ },
335
+ Q: () => {
336
+ const untilWk = limits?.seven_day?.resets_at;
337
+ const s = formatTimeUntil(untilWk);
338
+ emit(`${prefix("⏱", "7d-resets")}${c.bold(s !== "" ? s : "-")}`);
339
+ },
340
+ /* ==== COSTS ==== */
341
+ H: () => {
342
+ const sessDurMs = data.cost?.total_duration_ms ?? 0;
343
+ if (sessDurMs > 0)
344
+ emit(`${prefix("⏱", "elapsed")}${c.bold(formatHoursMinutes(sessDurMs))}`);
345
+ },
346
+ X: () => {
347
+ const sessCost = data.cost?.total_cost_usd;
348
+ if (sessCost !== undefined)
349
+ emit(`${prefix("$", "cost")}${c.bold(formatCostUsd(sessCost))}`);
350
+ },
351
+ /* ==== VERSION CONTROL ==== */
352
+ a: () => {
353
+ const linesAdded = data.cost?.total_lines_added ?? 0;
354
+ emit(`${prefix("⊕", "added")}${c.bold(String(linesAdded))}`);
355
+ },
356
+ r: () => {
357
+ const linesRemoved = data.cost?.total_lines_removed ?? 0;
358
+ emit(`${prefix("⊖", "removed")}${c.bold(String(linesRemoved))}`);
359
+ },
360
+ b: () => {
361
+ const g = getGit();
362
+ const label = g.branch !== "" ? g.branch : "no git";
363
+ emit(`${prefix("⎇", "branch")}${c.bold(label)}`);
364
+ },
365
+ g: () => {
366
+ const g = getGit();
367
+ if (g.branch !== "")
368
+ emit(`${prefix("±", "changed")}${c.bold(`+${g.added}/-${g.removed}`)}`);
369
+ },
370
+ G: () => {
371
+ const g = getGit();
372
+ if (g.branch !== "")
373
+ emit(`${prefix("⁈", "untracked")}${c.bold(String(g.untracked))}`);
374
+ },
375
+ d: () => {
376
+ const cwd = data.workspace?.current_dir ?? "";
377
+ if (cwd !== "")
378
+ emit(`${prefix("▶", "cwd")}${c.bold(cwd)}`);
379
+ },
380
+ /* ==== RESOURCES ==== */
381
+ M: () => {
382
+ const m = getMem();
383
+ if (m.total > 0)
384
+ emit(`${prefix("⛁", "mem")}${c.bold(`${formatBytes(m.used)}/${formatBytes(m.total)}`)}`);
385
+ },
386
+ /* ==== VERSIONS ==== */
387
+ V: () => {
388
+ const toolVersion = data.version ?? "";
389
+ const aseVersion = pkg.version ?? "";
390
+ let version = "";
391
+ if (toolVersion !== "")
392
+ version += `${tool}/${toolVersion}`;
393
+ if (aseVersion !== "")
394
+ version += `${version !== "" ? " " : ""}ase/${aseVersion}`;
395
+ emit(`${prefix("⎈", "version")}${c.bold(version)}`);
396
+ }
397
+ };
398
+ };
249
399
  /* command-line handling */
250
400
  export default class StatuslineCommand {
251
401
  log;
252
402
  constructor(log) {
253
403
  this.log = log;
254
404
  }
255
- /* parse and validate the --tool option */
256
- parseTool(value) {
257
- if (value !== "claude" && value !== "copilot" && value !== "codex")
258
- throw new InvalidArgumentError(`invalid --tool value: "${value}" (expected "claude", "copilot", or "codex")`);
259
- return value;
260
- }
261
405
  /* register commands */
262
406
  register(program) {
263
407
  /* default for --tool derived from ASE_TOOL environment variable
@@ -280,7 +424,7 @@ export default class StatuslineCommand {
280
424
  "(default: single line \"%m %e %t\")")
281
425
  .action(async (lines, opts) => {
282
426
  /* validate target tool */
283
- const tool = this.parseTool(opts.tool);
427
+ const tool = parseTool(opts.tool);
284
428
  /* OpenAI Codex CLI has no scriptable custom statusline
285
429
  mechanism, so reject the request explicitly */
286
430
  if (tool === "codex")
@@ -361,7 +505,7 @@ export default class StatuslineCommand {
361
505
  boxing = get("project.boxing", boxing);
362
506
  lifecycle = get("project.task.lifecycle", lifecycle);
363
507
  }
364
- catch (_e) {
508
+ catch {
365
509
  /* cascade unavailable; keep env-var fallbacks */
366
510
  }
367
511
  try {
@@ -369,7 +513,7 @@ export default class StatuslineCommand {
369
513
  state.read("lenient");
370
514
  taskId = String(state.get("task") ?? "").trim() || taskId;
371
515
  }
372
- catch (_e) {
516
+ catch {
373
517
  /* session state unavailable; keep env-var fallback */
374
518
  }
375
519
  return { taskId, persona, guidance, boxing, lifecycle };
@@ -392,152 +536,8 @@ export default class StatuslineCommand {
392
536
  && /^gpt-.+/i.test(getModel().name)
393
537
  && tmpl.some((line) => /%[SDWQ]/.test(line)))
394
538
  limits = await fetchCodexLimits();
395
- /* render a configuration value (if non-empty) with its icon and label */
396
- const cfgValue = (key, icon, label) => () => {
397
- const value = getCfg()[key];
398
- if (value !== "")
399
- emit(`${prefix(icon, label)}${c.bold(value)}`);
400
- };
401
- /* identifier to renderer map: each callback fetches its own information
402
- directly from data (or via the lazy helpers above for shared values) */
403
- const renderers = {
404
- /* ==== SCOPE ==== */
405
- u: () => {
406
- const user = process.env.USER ?? process.env.LOGNAME ?? "unknown";
407
- emit(`${prefix("※", "user")}${c.bold(user)}`);
408
- },
409
- p: () => {
410
- const id = projectIdOf(data.workspace?.current_dir ?? "");
411
- emit(`${prefix("⚑", "project")}${c.bold(id)}`);
412
- },
413
- T: cfgValue("taskId", "◉", "task"),
414
- s: () => emit(`${prefix("⏻", "session")}${c.bold(getSession())}`),
415
- /* ==== MODEL ==== */
416
- m: () => {
417
- const { name } = getModel();
418
- emit(`${prefix("⚙", "model")}${c.bold(name)}`);
419
- },
420
- e: () => {
421
- const { effort } = getModel();
422
- emit(`${prefix("⚒", "effort")}${c.bold(effort !== "" ? effort : "unknown")}`);
423
- },
424
- t: () => {
425
- const thinking = data.thinking?.enabled === true ? "yes" : "no";
426
- emit(`${prefix("⚛", "thinking")}${c.bold(thinking)}`);
427
- },
428
- /* ==== OUTPUT ==== */
429
- O: () => {
430
- const styleName = data.output_style?.name ?? "";
431
- if (styleName !== "")
432
- emit(`${prefix("≡", "style")}${c.bold(styleName)}`);
433
- },
434
- P: cfgValue("persona", "☯", "persona"),
435
- h: cfgValue("guidance", "▶", "guidance"),
436
- B: cfgValue("boxing", "▢", "boxing"),
437
- L: cfgValue("lifecycle", "⟳", "lifecycle"),
438
- /* ==== CONTEXT ==== */
439
- c: () => {
440
- const pct = Math.min(100, Math.max(0, Math.floor(data.context_window?.used_percentage ?? 0)));
441
- const barSize = 20;
442
- const filled = Math.round(pct / 100 * barSize);
443
- const bar = "█".repeat(filled) + "░".repeat(barSize - filled);
444
- emit(`${prefix("◔", "context")}${bar} ${pct}%`);
445
- },
446
- C: () => {
447
- /* probe the live-context fields first and fall back to the
448
- cumulative ones, so each harness contributes its own view:
449
- GitHub Copilot CLI reports the live window separately (its
450
- "total_*" are session sums), whereas Anthropic Claude Code CLI
451
- folds it into "total_input_tokens". Output tokens stay out --
452
- "used_percentage" is input-side only. */
453
- const cw = data.context_window;
454
- const pct = cw?.used_percentage ?? 0;
455
- const tokensCur = cw?.current_context_tokens ??
456
- cw?.last_call_input_tokens ?? cw?.total_input_tokens ?? 0;
457
- const tokensLim = cw?.displayed_context_limit ?? cw?.context_window_size ??
458
- (pct > 0 && tokensCur > 0 ? Math.round(tokensCur * 100 / pct) : 0);
459
- if (tokensLim > 0)
460
- emit(`${prefix("◆", "tokens")}${c.bold(formatTokens(tokensCur) + "/" + formatTokens(tokensLim))}`);
461
- },
462
- /* ==== RATE LIMITS ==== */
463
- S: () => {
464
- const pct5h = limits?.five_hour?.used_percentage;
465
- const s = pct5h !== undefined ? `${pct5h.toFixed(1)}%` : "-";
466
- emit(`${prefix("⏲", "5h-usage")}${c.bold(s)}`);
467
- },
468
- D: () => {
469
- const until5h = limits?.five_hour?.resets_at;
470
- const s = formatTimeUntil(until5h);
471
- emit(`${prefix("⏱", "5h-resets")}${c.bold(s !== "" ? s : "-")}`);
472
- },
473
- W: () => {
474
- const pctWk = limits?.seven_day?.used_percentage;
475
- const s = pctWk !== undefined ? `${pctWk.toFixed(1)}%` : "-";
476
- emit(`${prefix("⏲", "7d-usage")}${c.bold(s)}`);
477
- },
478
- Q: () => {
479
- const untilWk = limits?.seven_day?.resets_at;
480
- const s = formatTimeUntil(untilWk);
481
- emit(`${prefix("⏱", "7d-resets")}${c.bold(s !== "" ? s : "-")}`);
482
- },
483
- /* ==== COSTS ==== */
484
- H: () => {
485
- const sessDurMs = data.cost?.total_duration_ms ?? 0;
486
- if (sessDurMs > 0)
487
- emit(`${prefix("⏱", "elapsed")}${c.bold(formatHoursMinutes(sessDurMs))}`);
488
- },
489
- X: () => {
490
- const sessCost = data.cost?.total_cost_usd;
491
- if (sessCost !== undefined)
492
- emit(`${prefix("$", "cost")}${c.bold(formatCostUsd(sessCost))}`);
493
- },
494
- /* ==== VERSION CONTROL ==== */
495
- a: () => {
496
- const linesAdded = data.cost?.total_lines_added ?? 0;
497
- emit(`${prefix("⊕", "added")}${c.bold(String(linesAdded))}`);
498
- },
499
- r: () => {
500
- const linesRemoved = data.cost?.total_lines_removed ?? 0;
501
- emit(`${prefix("⊖", "removed")}${c.bold(String(linesRemoved))}`);
502
- },
503
- b: () => {
504
- const g = getGit();
505
- const label = g.branch !== "" ? g.branch : "no git";
506
- emit(`${prefix("⎇", "branch")}${c.bold(label)}`);
507
- },
508
- g: () => {
509
- const g = getGit();
510
- if (g.branch !== "")
511
- emit(`${prefix("±", "changed")}${c.bold(`+${g.added}/-${g.removed}`)}`);
512
- },
513
- G: () => {
514
- const g = getGit();
515
- if (g.branch !== "")
516
- emit(`${prefix("⁈", "untracked")}${c.bold(String(g.untracked))}`);
517
- },
518
- d: () => {
519
- const cwd = data.workspace?.current_dir ?? "";
520
- if (cwd !== "")
521
- emit(`${prefix("▶", "cwd")}${c.bold(cwd)}`);
522
- },
523
- /* ==== RESOURCES ==== */
524
- M: () => {
525
- const m = getMem();
526
- if (m.total > 0)
527
- emit(`${prefix("⛁", "mem")}${c.bold(`${formatBytes(m.used)}/${formatBytes(m.total)}`)}`);
528
- },
529
- /* ==== VERSIONS ==== */
530
- V: () => {
531
- const toolVersion = data.version ?? "";
532
- const aseVersion = pkg.version ?? "";
533
- let version = "";
534
- if (toolVersion !== "")
535
- version += `${tool}/${toolVersion}`;
536
- if (aseVersion !== "")
537
- version += `${version !== "" ? " " : ""}ase/${aseVersion}`;
538
- emit(`${prefix("⎈", "version")}${c.bold(version)}`);
539
- }
540
- };
539
+ /* identifier to renderer map (see "createRenderers") */
540
+ const renderers = createRenderers({ data, tool, limits, emit, prefix, getSession, getCfg, getModel, getGit, getMem });
541
541
  /* walk each template line and render */
542
542
  const closeSpan = () => {
543
543
  span = null;
@@ -579,8 +579,8 @@ export default class StatuslineCommand {
579
579
  if (opts.padding > 0) {
580
580
  const pad = " ".repeat(opts.padding);
581
581
  out = out.split("\n").map((line) => line !== "" ? `${pad}${line}${pad}` : line).join("\n");
582
- /* for Copilot, prefix with a zero-width non-breaking space
583
- to prevent the first line to be unindented */
582
+ /* for Copilot, prefix with a zero-width space
583
+ to prevent the first line from being unindented */
584
584
  if (tool === "copilot")
585
585
  out = "\u200B" + out;
586
586
  }
@@ -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;
@@ -133,12 +133,13 @@ const cardName = (card) => card.agents.length > 1 ? `${card.name} ×${card.agent
133
133
  /* remove control characters from a text of the agent relay */
134
134
  const clean = (text) => text.replace(/\p{Cc}/gu, "");
135
135
  /* draw a cell-unit layout of the agent graph onto a character grid, returning per cell the
136
- character and a tone for coloring (like the task graph): the state nodes in tone "done" (or "sel"), the
137
- agent cards in tone "active" (busy), "idle" (waiting), or "sel" (selected, with a heavy border, like
138
- a selected state), with the name of
139
- the agent inverse on their first line, followed by their field lines (with the tone suffix
140
- "-title" on the labels, "-value" on their bold values, and the busy task ids inverse), and the given pulse glyph (else a filled
141
- circle) of a busy card, resp. an empty circle of a waiting one, in the right part of their top border */
136
+ character and a tone for coloring (like the task graph): the state nodes in tone "done"
137
+ (or "sel"), the agent cards in tone "active" (busy), "idle" (waiting), or "sel" (selected,
138
+ with a heavy border, like a selected state), with the name of the agent inverse on their
139
+ first line, followed by their field lines (with the tone suffix "-title" on the labels,
140
+ "-value" on their bold values, and the busy task ids inverse), and the given pulse glyph
141
+ (else a filled circle) of a busy card, resp. an empty circle of a waiting one, in the
142
+ right part of their top border */
142
143
  export const drawFlowText = (flow, layout, selected, pulse = "", beat = () => false) => {
143
144
  const g = textGrid(layout);
144
145
  const { W, H, chars, tones } = g;
@@ -20,10 +20,11 @@ export const units = {
20
20
  /* the id prefix of the compound node of a group (task ids never contain a colon) */
21
21
  const GROUP = "group:";
22
22
  /* the label lines of a node (in cell units only) of a given box width: the
23
- task id (behind "≡", and of an epic also behind "♛") and title, cut onto a single line, or with titles shown, wrapped
24
- onto at most three lines, with a placeholder column glued in front of
25
- the task id to reserve the extra column of its inverse rendering (plus two for
26
- the "≡", and two more for the "♛" of an epic), and the arrow in front of the title replaced by a placeholder
23
+ task id (behind "≡", and of an epic also behind "♛") and title, cut onto a
24
+ single line, or with titles shown, wrapped onto at most three lines, with a
25
+ placeholder column glued in front of the task id to reserve the extra column
26
+ of its inverse rendering (plus two for the "≡", and two more for the "♛" of
27
+ an epic), and the arrow in front of the title replaced by a placeholder
27
28
  column glued to the title */
28
29
  export const nodeLines = (board, id, titles, width) => {
29
30
  const card = board.cards.get(id);
@@ -281,14 +281,13 @@ export const handleMouse = (ctx, btn, mx, my) => {
281
281
  setDialog({ id: hit, tab: 0, first: 0, scrolls: {} });
282
282
  }
283
283
  };
284
- /* handle a key press onto the deletion confirmation, the transfer popup, the assign
285
- popup, the prompt popup, or the filter field (outside of the read dialog) */
286
- const handlePopupKey = (ctx, input, key) => {
287
- const { dialog, setFilter, typing, setTyping, setNotice, confirm, setConfirm, transfer, setTransfer, transferList, remove, transferTo, assign, setAssign, assignTo, prompt, setPrompt, cancelPrompt, submitPrompt } = ctx;
284
+ /* handle a key press onto the prompt popup */
285
+ const handlePromptKey = (ctx, prompt, input, key) => {
286
+ const { setPrompt, cancelPrompt, submitPrompt } = ctx;
288
287
  /* operate the prompt popup: TAB resp. Shift+TAB switch the focus to the next resp.
289
288
  previous field (the request name of a request by prompt, the prompt unless ignored, and
290
289
  the request and cancel buttons), and ESC cancels in every field */
291
- if (prompt !== null && (key.tab || key.escape)) {
290
+ if (key.tab || key.escape) {
292
291
  if (key.escape)
293
292
  cancelPrompt();
294
293
  else {
@@ -301,7 +300,7 @@ const handlePopupKey = (ctx, input, key) => {
301
300
  }
302
301
  /* press the focused button of the prompt popup: RETURN (or SPACE) presses it,
303
302
  and the left/right arrows switch between the request and cancel buttons */
304
- if (prompt !== null && (prompt.focus === "request" || prompt.focus === "cancel")) {
303
+ if (prompt.focus === "request" || prompt.focus === "cancel") {
305
304
  if ((key.return || input === " ") && prompt.focus === "request")
306
305
  submitPrompt();
307
306
  else if (key.return || input === " ")
@@ -310,17 +309,7 @@ const handlePopupKey = (ctx, input, key) => {
310
309
  setPrompt({ ...prompt, focus: prompt.focus === "request" ? "cancel" : "request" });
311
310
  return;
312
311
  }
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
- if (prompt !== null && prompt.request !== null && prompt.focus === "name") {
312
+ if (prompt.request !== null && prompt.focus === "name") {
324
313
  /* type the request name of a request by prompt: RETURN switches to the prompt (resp. to
325
314
  the request button if the prompt is ignored), the
326
315
  up/down arrows cycle through the suggested request names of the drop-down (matching
@@ -342,36 +331,53 @@ const handlePopupKey = (ctx, input, key) => {
342
331
  name(request.name + input.toLowerCase());
343
332
  return;
344
333
  }
345
- if (prompt !== null) {
346
- const chars = [...prompt.text];
347
- const pos = Math.max(0, Math.min(prompt.pos, chars.length));
348
- const insert = (text) => {
349
- const ins = [...text];
350
- chars.splice(pos, 0, ...ins);
351
- setPrompt({ ...prompt, text: chars.join(""), pos: pos + ins.length });
352
- };
353
- if ((key.return && (key.shift || key.meta)) || (key.ctrl && input === "j"))
354
- insert("\n");
355
- else if (key.return)
356
- submitPrompt();
357
- else if (key.leftArrow || key.rightArrow)
358
- setPrompt({ ...prompt, pos: Math.max(0, Math.min(chars.length, pos + (key.leftArrow ? -1 : 1))) });
359
- else if (key.home || key.end)
360
- setPrompt({ ...prompt, pos: key.home ? 0 : chars.length });
361
- else if (key.backspace) {
362
- if (pos > 0) {
363
- chars.splice(pos - 1, 1);
364
- setPrompt({ ...prompt, text: chars.join(""), pos: pos - 1 });
365
- }
334
+ /* type the prompt of an edit request: RETURN requests the edit, Shift+RETURN
335
+ (under the kitty keyboard protocol only), Alt+RETURN (under the kitty keyboard
336
+ protocol, or where the terminal sends it as ESC+RETURN, e.g. with "Option as Meta"
337
+ on macOS), and Ctrl+J (in every terminal, as a plain linefeed without the kitty
338
+ keyboard protocol) insert a newline,
339
+ the left/right arrows move the cursor by one character, HOME/END move it
340
+ to the start/end, BACKSPACE removes the character before the cursor, DELETE
341
+ removes the character at the cursor, and all other input (also pasted text,
342
+ with its newlines kept and its other control characters turned into spaces)
343
+ is inserted at the cursor */
344
+ const chars = [...prompt.text];
345
+ const pos = Math.max(0, Math.min(prompt.pos, chars.length));
346
+ const insert = (text) => {
347
+ const ins = [...text];
348
+ chars.splice(pos, 0, ...ins);
349
+ setPrompt({ ...prompt, text: chars.join(""), pos: pos + ins.length });
350
+ };
351
+ if ((key.return && (key.shift || key.meta)) || (key.ctrl && input === "j"))
352
+ insert("\n");
353
+ else if (key.return)
354
+ submitPrompt();
355
+ else if (key.leftArrow || key.rightArrow)
356
+ setPrompt({ ...prompt, pos: Math.max(0, Math.min(chars.length, pos + (key.leftArrow ? -1 : 1))) });
357
+ else if (key.home || key.end)
358
+ setPrompt({ ...prompt, pos: key.home ? 0 : chars.length });
359
+ else if (key.backspace) {
360
+ if (pos > 0) {
361
+ chars.splice(pos - 1, 1);
362
+ setPrompt({ ...prompt, text: chars.join(""), pos: pos - 1 });
366
363
  }
367
- else if (key.delete) {
368
- if (pos < chars.length) {
369
- chars.splice(pos, 1);
370
- setPrompt({ ...prompt, text: chars.join(""), pos });
371
- }
364
+ }
365
+ else if (key.delete) {
366
+ if (pos < chars.length) {
367
+ chars.splice(pos, 1);
368
+ setPrompt({ ...prompt, text: chars.join(""), pos });
372
369
  }
373
- else if (!key.ctrl && !key.meta && input !== "")
374
- insert(input.replace(/\r\n?/g, "\n").replace(/[^\P{Cc}\n]/gu, " "));
370
+ }
371
+ else if (!key.ctrl && !key.meta && input !== "")
372
+ insert(input.replace(/\r\n?/g, "\n").replace(/[^\P{Cc}\n]/gu, " "));
373
+ };
374
+ /* handle a key press onto the deletion confirmation, the transfer popup, the assign
375
+ popup, the prompt popup, or the filter field (outside of the read dialog) */
376
+ const handlePopupKey = (ctx, input, key) => {
377
+ const { dialog, setFilter, typing, setTyping, setNotice, confirm, setConfirm, transfer, setTransfer, transferList, remove, transferTo, assign, setAssign, assignTo, prompt } = ctx;
378
+ /* operate the prompt popup */
379
+ if (prompt !== null) {
380
+ handlePromptKey(ctx, prompt, input, key);
375
381
  return;
376
382
  }
377
383
  /* answer the confirmation of a task deletion: "y" deletes, ESC cancels, the