@herbertgao/pi-subagents 0.15.3 → 0.15.4

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.
@@ -11,6 +11,7 @@
11
11
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent"
12
12
  import type { SubagentScheduler } from "../schedule.js"
13
13
  import type { ScheduledSubagent } from "../types.js"
14
+ import { selectItem } from "./select-item.js"
14
15
 
15
16
  /** Format an ISO timestamp as relative time ("in 4h", "2d ago", "—"). */
16
17
  function relTime(iso: string | undefined, now = Date.now()): string {
@@ -88,16 +89,16 @@ export async function showSchedulesMenu(
88
89
  return
89
90
  }
90
91
 
91
- const labels = jobs.map((j) => formatJob(j, scheduler))
92
- const choice = await ctx.ui.select(
92
+ // Numbered + item-paired: two jobs whose names agree in the first 18
93
+ // characters format identically, and matching the returned string back
94
+ // against a parallel label array cancelled whichever came first.
95
+ const job = await selectItem(
96
+ ctx.ui,
93
97
  `Scheduled jobs (${jobs.length}) — select to cancel`,
94
- labels,
98
+ jobs,
99
+ (j) => formatJob(j, scheduler),
95
100
  )
96
- if (!choice) return
97
-
98
- const idx = labels.indexOf(choice)
99
- if (idx < 0) return
100
- const job = jobs[idx]
101
+ if (!job) return
101
102
 
102
103
  const ok = await ctx.ui.confirm(
103
104
  `Cancel "${job.name}"?`,
@@ -0,0 +1,48 @@
1
+ /**
2
+ * select-item.ts — pick an item from a list via `ctx.ui.select`, safely.
3
+ *
4
+ * Pi's dialog API is `select(title, options: string[]) => Promise<string | undefined>`:
5
+ * strings in, string out, with no index or value form. Callers therefore have to
6
+ * map the returned string back to the item it came from, and the obvious way —
7
+ * `labels.indexOf(choice)` over a parallel array — silently resolves to the
8
+ * FIRST match whenever two rows format identically. Row formatters here truncate
9
+ * (job names to 18 chars, agent descriptions to whatever fits), and the text they
10
+ * truncate is LLM-authored, so collisions are ordinary rather than exotic.
11
+ *
12
+ * This numbers every row, which makes the labels unique by construction — no
13
+ * data-dependent branch that only executes in the case nobody exercises — and
14
+ * keeps each label paired with its item so a later edit that sorts or filters
15
+ * between building and resolving cannot desync them.
16
+ */
17
+
18
+ /** Minimal shape of the `ctx.ui` surface this needs. */
19
+ export interface SelectUI {
20
+ select(title: string, options: string[]): Promise<string | undefined>
21
+ }
22
+
23
+ /**
24
+ * Show a numbered picker and return the chosen item (not its label).
25
+ *
26
+ * Returns undefined when the user escapes, or when the returned string is not
27
+ * one we offered.
28
+ */
29
+ export async function selectItem<T>(
30
+ ui: SelectUI,
31
+ title: string,
32
+ items: readonly T[],
33
+ format: (item: T, index: number) => string,
34
+ ): Promise<T | undefined> {
35
+ // Pad the number so a 10+ item list keeps its columns aligned.
36
+ const width = String(items.length).length
37
+ const rows = items.map((item, i) => ({
38
+ item,
39
+ label: `${String(i + 1).padStart(width)}. ${format(item, i)}`,
40
+ }))
41
+
42
+ const choice = await ui.select(
43
+ title,
44
+ rows.map((r) => r.label),
45
+ )
46
+ if (!choice) return undefined
47
+ return rows.find((r) => r.label === choice)?.item
48
+ }