@herbertgao/pi-subagents 0.15.3 → 0.15.5
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.
- package/CHANGELOG.md +18 -0
- package/package.json +8 -4
- package/src/agent-color.ts +72 -67
- package/src/agent-file-toggle.ts +255 -0
- package/src/agent-manager.ts +191 -3
- package/src/agent-runner.ts +20 -4
- package/src/index.ts +289 -113
- package/src/output-file.ts +24 -1
- package/src/ui/agent-widget.ts +24 -2
- package/src/ui/fleet-list.ts +15 -6
- package/src/ui/schedule-menu.ts +9 -8
- package/src/ui/select-item.ts +48 -0
- package/src/worktree.ts +9 -5
package/src/ui/fleet-list.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
truncateToWidth,
|
|
20
20
|
visibleWidth,
|
|
21
21
|
} from "@earendil-works/pi-tui"
|
|
22
|
-
import { renderAgentName } from "../agent-color.js"
|
|
22
|
+
import { hasAgentBadge, renderAgentName } from "../agent-color.js"
|
|
23
23
|
import type { AgentManager } from "../agent-manager.js"
|
|
24
24
|
import type { AgentRecord } from "../types.js"
|
|
25
25
|
import { getLifetimeTotal } from "../usage.js"
|
|
@@ -476,15 +476,24 @@ export class FleetList {
|
|
|
476
476
|
width: number,
|
|
477
477
|
theme: Theme,
|
|
478
478
|
): string {
|
|
479
|
-
const
|
|
479
|
+
const selected = rosterIndex === sel
|
|
480
|
+
const name = renderAgentName(
|
|
481
|
+
record.type,
|
|
482
|
+
theme,
|
|
483
|
+
selected
|
|
484
|
+
? { fallbackColor: "text", bold: hasAgentBadge(record.type) }
|
|
485
|
+
: { fallbackColor: "muted" },
|
|
486
|
+
)
|
|
487
|
+
const description = selected
|
|
488
|
+
? theme.fg("text", record.description)
|
|
489
|
+
: record.description
|
|
490
|
+
const left = ` ${this.bullet(rosterIndex, sel, theme)} ${name} ${description}`
|
|
480
491
|
const tokens = getLifetimeTotal(
|
|
481
492
|
this.agentActivity.get(record.id)?.lifetimeUsage ?? record.lifetimeUsage,
|
|
482
493
|
)
|
|
483
494
|
const elapsedMs = (record.completedAt ?? Date.now()) - record.startedAt // freezes once finished
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
`${formatFleetElapsed(elapsedMs)} · ${formatFleetTokens(tokens)}`,
|
|
487
|
-
)
|
|
495
|
+
const stats = `${formatFleetElapsed(elapsedMs)} · ${formatFleetTokens(tokens)}`
|
|
496
|
+
const right = selected ? theme.fg("text", stats) : theme.fg("dim", stats)
|
|
488
497
|
return rightAlign(left, right, width)
|
|
489
498
|
}
|
|
490
499
|
}
|
package/src/ui/schedule-menu.ts
CHANGED
|
@@ -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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
98
|
+
jobs,
|
|
99
|
+
(j) => formatJob(j, scheduler),
|
|
95
100
|
)
|
|
96
|
-
if (!
|
|
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
|
+
}
|
package/src/worktree.ts
CHANGED
|
@@ -134,11 +134,15 @@ export function cleanupWorktree(
|
|
|
134
134
|
// Truncate description for commit message (no shell sanitization needed — execFileSync uses argv)
|
|
135
135
|
const safeDesc = agentDescription.slice(0, 200)
|
|
136
136
|
const commitMsg = `pi-agent: ${safeDesc}`
|
|
137
|
-
execFileSync(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
execFileSync(
|
|
138
|
+
"git",
|
|
139
|
+
["commit", "--no-verify", "--no-gpg-sign", "-m", commitMsg],
|
|
140
|
+
{
|
|
141
|
+
cwd: worktree.path,
|
|
142
|
+
stdio: "pipe",
|
|
143
|
+
timeout: 10000,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
142
146
|
} else {
|
|
143
147
|
const currentSha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
144
148
|
cwd: worktree.path,
|