@nredd/pi-subagents 0.0.0-stage → 0.22.0-nredd.6
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/LICENSE +21 -0
- package/README.md +1244 -2
- package/package.json +75 -4
- package/src/abortable.ts +43 -0
- package/src/agent-color.ts +161 -0
- package/src/agent-file-toggle.ts +297 -0
- package/src/agent-manager.ts +1874 -0
- package/src/agent-runner.ts +1552 -0
- package/src/agent-source-badge.ts +75 -0
- package/src/agent-types.ts +346 -0
- package/src/child-context.ts +15 -0
- package/src/clock.ts +25 -0
- package/src/completion-outbox.ts +125 -0
- package/src/context.ts +58 -0
- package/src/cross-extension-rpc.ts +221 -0
- package/src/custom-agents.ts +436 -0
- package/src/default-agents.ts +126 -0
- package/src/detached-results.ts +134 -0
- package/src/enabled-models.ts +180 -0
- package/src/enterprise-local.ts +66 -0
- package/src/env.ts +33 -0
- package/src/group-join.ts +148 -0
- package/src/index.ts +4423 -0
- package/src/invocation-config.ts +189 -0
- package/src/memory.ts +179 -0
- package/src/mention-clone.ts +195 -0
- package/src/mention.ts +141 -0
- package/src/model-call.ts +83 -0
- package/src/model-resolver.ts +266 -0
- package/src/model-scope.ts +70 -0
- package/src/nested-tools.ts +448 -0
- package/src/output-file.ts +164 -0
- package/src/package-resources.ts +624 -0
- package/src/prompts.ts +156 -0
- package/src/quota-admission.ts +99 -0
- package/src/schedule-store.ts +153 -0
- package/src/schedule.ts +446 -0
- package/src/settings.ts +728 -0
- package/src/skill-loader.ts +137 -0
- package/src/startup-error.ts +29 -0
- package/src/status-note.ts +90 -0
- package/src/structured-output.ts +130 -0
- package/src/types.ts +404 -0
- package/src/ui/agent-mention.ts +216 -0
- package/src/ui/agent-row.ts +235 -0
- package/src/ui/agent-widget.ts +692 -0
- package/src/ui/conversation-viewer.ts +832 -0
- package/src/ui/fleet-list.ts +675 -0
- package/src/ui/notification-view.ts +120 -0
- package/src/ui/schedule-menu.ts +105 -0
- package/src/ui/select-item.ts +45 -0
- package/src/ui/viewer-keys.ts +39 -0
- package/src/ui/workflow-card.ts +462 -0
- package/src/ui/workflow-dialog.ts +1115 -0
- package/src/ui/workflow-menu.ts +193 -0
- package/src/usage.ts +167 -0
- package/src/workflow/collisions.ts +123 -0
- package/src/workflow/entry.ts +47 -0
- package/src/workflow/host.ts +401 -0
- package/src/workflow/journal.ts +164 -0
- package/src/workflow/json-schema.ts +129 -0
- package/src/workflow/meta.ts +326 -0
- package/src/workflow/progress.ts +549 -0
- package/src/workflow/runtime.ts +1216 -0
- package/src/workflow/saved.ts +259 -0
- package/src/workflow/task.ts +302 -0
- package/src/workflow/tool-description.ts +200 -0
- package/src/workflow/worker-source.ts +781 -0
- package/src/worktree.ts +230 -0
- package/src/xml.ts +13 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notification-view.ts -- the `subagent-notification` message renderer.
|
|
3
|
+
*
|
|
4
|
+
* Core gives a custom message with a registered renderer a ▸/▾ gutter, collapses
|
|
5
|
+
* it to the renderer's first line, and toggles it on click. So the first line is
|
|
6
|
+
* the whole collapsed notice and no marker is drawn here:
|
|
7
|
+
*
|
|
8
|
+
* ✓ audit the RPC path · 12 tools · 33.8k token · ~$0.0042 · 41.2s
|
|
9
|
+
* 3 agents finished · ✓ a, ✓ b, ✗ c
|
|
10
|
+
*
|
|
11
|
+
* Expanded adds the stats, the result preview and the transcript as a link.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
15
|
+
import { type Component, hyperlink, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
16
|
+
import type { NotificationDetails } from "../types.js";
|
|
17
|
+
import { formatCost, formatMs, formatTokens, formatTurns, type Theme } from "./agent-widget.js";
|
|
18
|
+
|
|
19
|
+
/** Lines of the result preview shown when expanded. */
|
|
20
|
+
const EXPANDED_PREVIEW_LINES = 30;
|
|
21
|
+
|
|
22
|
+
const FAILED = new Set(["error", "stopped", "aborted"]);
|
|
23
|
+
|
|
24
|
+
function icon(d: NotificationDetails, theme: Theme): string {
|
|
25
|
+
return FAILED.has(d.status) ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The parts after the description: tools, tokens, cost, duration. */
|
|
29
|
+
function statParts(d: NotificationDetails, showCost: boolean): string[] {
|
|
30
|
+
const parts: string[] = [];
|
|
31
|
+
if (d.toolUses > 0) parts.push(`${d.toolUses} tool${d.toolUses === 1 ? "" : "s"}`);
|
|
32
|
+
if (d.totalTokens > 0) parts.push(formatTokens(d.totalTokens));
|
|
33
|
+
if (showCost) {
|
|
34
|
+
const cost = formatCost(d.totalCost ?? 0);
|
|
35
|
+
if (cost) parts.push(cost);
|
|
36
|
+
}
|
|
37
|
+
if (d.durationMs > 0) parts.push(formatMs(d.durationMs));
|
|
38
|
+
return parts;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `✓ desc · 12 tools · 33.8k token · ~$0.0042 · 41.2s`. */
|
|
42
|
+
export function noticeSummary(d: NotificationDetails, theme: Theme, showCost: boolean): string {
|
|
43
|
+
const dot = ` ${theme.fg("dim", "·")} `;
|
|
44
|
+
const head = `${icon(d, theme)} ${theme.bold(d.description)}`;
|
|
45
|
+
const stats = statParts(d, showCost).map(p => theme.fg("dim", p));
|
|
46
|
+
return [head, ...stats].join(dot);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `3 agents finished · ✓ a, ✓ b, ✗ c`. */
|
|
50
|
+
export function groupSummary(all: NotificationDetails[], theme: Theme): string {
|
|
51
|
+
const names = all.map(d => `${icon(d, theme)} ${d.description}`).join(theme.fg("dim", ", "));
|
|
52
|
+
return `${theme.bold(`${all.length} agents finished`)} ${theme.fg("dim", "·")} ${names}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The transcript as an OSC 8 hyperlink, so a click opens it. */
|
|
56
|
+
function transcriptLine(path: string, theme: Theme): string {
|
|
57
|
+
return `${hyperlink("transcript ↗", pathToFileURL(path).href)} ${theme.fg("dim", path)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Detail lines for one agent, shown only when expanded. */
|
|
61
|
+
function detailLines(d: NotificationDetails, theme: Theme, showCost: boolean, indent: string): string[] {
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
const stats: string[] = [];
|
|
64
|
+
if (d.status !== "completed") {
|
|
65
|
+
stats.push(d.status === "steered" ? "completed (steered)" : d.status);
|
|
66
|
+
}
|
|
67
|
+
if (d.turnCount > 0) stats.push(formatTurns(d.turnCount, d.maxTurns));
|
|
68
|
+
stats.push(...statParts(d, showCost));
|
|
69
|
+
if (stats.length > 0) lines.push(indent + theme.fg("dim", stats.join(" · ")));
|
|
70
|
+
if (d.error) lines.push(indent + theme.fg("error", d.error));
|
|
71
|
+
for (const line of d.resultPreview.split("\n").slice(0, EXPANDED_PREVIEW_LINES)) {
|
|
72
|
+
lines.push(indent + theme.fg("dim", line));
|
|
73
|
+
}
|
|
74
|
+
if (d.outputFile) lines.push(indent + transcriptLine(d.outputFile, theme));
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** First line truncated (core folds on it), the rest wrapped. */
|
|
79
|
+
class NoticeView implements Component {
|
|
80
|
+
constructor(
|
|
81
|
+
private readonly first: string,
|
|
82
|
+
private readonly rest: string[],
|
|
83
|
+
) {}
|
|
84
|
+
|
|
85
|
+
render(width: number): string[] {
|
|
86
|
+
return [truncateToWidth(this.first, width), ...this.rest.flatMap(line => wrapTextWithAnsi(line, width))];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
invalidate(): void { /* static */ }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function renderNotification(
|
|
93
|
+
details: NotificationDetails,
|
|
94
|
+
expanded: boolean,
|
|
95
|
+
theme: Theme,
|
|
96
|
+
showCost: boolean,
|
|
97
|
+
): Component {
|
|
98
|
+
const all = [details, ...(details.others ?? [])];
|
|
99
|
+
if (all.length === 1) {
|
|
100
|
+
return new NoticeView(
|
|
101
|
+
noticeSummary(details, theme, showCost),
|
|
102
|
+
expanded ? detailLines(details, theme, showCost, " ") : [],
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const rest: string[] = [];
|
|
106
|
+
if (expanded) {
|
|
107
|
+
for (const d of all) {
|
|
108
|
+
rest.push(noticeSummary(d, theme, showCost));
|
|
109
|
+
rest.push(...detailLines(d, theme, showCost, " "));
|
|
110
|
+
}
|
|
111
|
+
if (showCost) {
|
|
112
|
+
const total = formatCost(all.reduce((sum, a) => sum + (a.totalCost ?? 0), 0));
|
|
113
|
+
if (total) {
|
|
114
|
+
const tokens = all.reduce((sum, a) => sum + a.totalTokens, 0);
|
|
115
|
+
rest.push(theme.fg("dim", `${formatTokens(tokens)} · ${total} in total`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return new NoticeView(groupSummary(all, theme), rest);
|
|
120
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schedule-menu.ts — `/agents → Scheduled jobs` submenu.
|
|
3
|
+
*
|
|
4
|
+
* Minimal v1 surface: list scheduled jobs, select one to inspect details +
|
|
5
|
+
* confirm cancellation. No create wizard (the `Agent` tool's `schedule` param
|
|
6
|
+
* is the canonical creation path), no toggle/cleanup (cancel is enough for
|
|
7
|
+
* "I scheduled something dumb, get rid of it"). Add management surfaces here
|
|
8
|
+
* if real demand emerges.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { SubagentScheduler } from "../schedule.js";
|
|
13
|
+
import type { ScheduledSubagent } from "../types.js";
|
|
14
|
+
import { selectItem } from "./select-item.js";
|
|
15
|
+
|
|
16
|
+
/** Format an ISO timestamp as relative time ("in 4h", "2d ago", "—"). */
|
|
17
|
+
function relTime(iso: string | undefined, now = Date.now()): string {
|
|
18
|
+
if (!iso) return "—";
|
|
19
|
+
const t = new Date(iso).getTime();
|
|
20
|
+
if (Number.isNaN(t)) return "—";
|
|
21
|
+
const diff = t - now;
|
|
22
|
+
const abs = Math.abs(diff);
|
|
23
|
+
const future = diff > 0;
|
|
24
|
+
if (abs < 60_000) return future ? "in <1m" : "<1m ago";
|
|
25
|
+
const m = Math.round(abs / 60_000);
|
|
26
|
+
if (m < 60) return future ? `in ${m}m` : `${m}m ago`;
|
|
27
|
+
const h = Math.round(abs / 3_600_000);
|
|
28
|
+
if (h < 24) return future ? `in ${h}h` : `${h}h ago`;
|
|
29
|
+
const d = Math.round(abs / 86_400_000);
|
|
30
|
+
return future ? `in ${d}d` : `${d}d ago`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One-line status icon. */
|
|
34
|
+
function statusIcon(j: ScheduledSubagent): string {
|
|
35
|
+
if (!j.enabled) return "✗";
|
|
36
|
+
if (j.lastStatus === "error") return "!";
|
|
37
|
+
if (j.lastStatus === "running") return "⋯";
|
|
38
|
+
return "✓";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Compact selectable row — name, schedule, agent type, next/last run, count. */
|
|
42
|
+
function formatJob(j: ScheduledSubagent, scheduler: SubagentScheduler): string {
|
|
43
|
+
const next = scheduler.getNextRun(j.id);
|
|
44
|
+
return [
|
|
45
|
+
statusIcon(j),
|
|
46
|
+
j.name.padEnd(18).slice(0, 18),
|
|
47
|
+
j.schedule.padEnd(14).slice(0, 14),
|
|
48
|
+
`[${j.subagent_type}]`,
|
|
49
|
+
`next ${relTime(next)}`,
|
|
50
|
+
`last ${relTime(j.lastRun)}`,
|
|
51
|
+
`runs ${j.runCount}`,
|
|
52
|
+
].join(" ");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Multi-line details block for the cancel confirm. */
|
|
56
|
+
function formatDetails(j: ScheduledSubagent, scheduler: SubagentScheduler): string {
|
|
57
|
+
const next = scheduler.getNextRun(j.id) ?? "—";
|
|
58
|
+
return [
|
|
59
|
+
`name: ${j.name}`,
|
|
60
|
+
`schedule: ${j.schedule} (${j.scheduleType})`,
|
|
61
|
+
`agent: ${j.subagent_type}`,
|
|
62
|
+
`prompt: ${j.prompt.slice(0, 200)}${j.prompt.length > 200 ? "…" : ""}`,
|
|
63
|
+
`created: ${j.createdAt}`,
|
|
64
|
+
`last run: ${j.lastRun ?? "—"} (${j.lastStatus ?? "—"})`,
|
|
65
|
+
`next run: ${next}`,
|
|
66
|
+
`runs: ${j.runCount}`,
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* List scheduled jobs; selecting one opens a cancel-confirm with details.
|
|
72
|
+
* Returns when the user backs out or after a cancellation.
|
|
73
|
+
*/
|
|
74
|
+
export async function showSchedulesMenu(
|
|
75
|
+
ctx: ExtensionCommandContext,
|
|
76
|
+
scheduler: SubagentScheduler,
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
if (!scheduler.isActive()) {
|
|
79
|
+
ctx.ui.notify("Scheduler is not active in this session.", "warning");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const jobs = scheduler.list();
|
|
84
|
+
if (jobs.length === 0) {
|
|
85
|
+
ctx.ui.notify("No scheduled jobs.", "info");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Numbered + item-paired: two jobs whose names agree in the first 18
|
|
90
|
+
// characters format identically, and matching the returned string back
|
|
91
|
+
// against a parallel label array cancelled whichever came first.
|
|
92
|
+
const job = await selectItem(
|
|
93
|
+
ctx.ui,
|
|
94
|
+
`Scheduled jobs (${jobs.length}) — select to cancel`,
|
|
95
|
+
jobs,
|
|
96
|
+
j => formatJob(j, scheduler),
|
|
97
|
+
);
|
|
98
|
+
if (!job) return;
|
|
99
|
+
|
|
100
|
+
const ok = await ctx.ui.confirm(`Cancel "${job.name}"?`, formatDetails(job, scheduler));
|
|
101
|
+
if (!ok) return;
|
|
102
|
+
|
|
103
|
+
scheduler.removeJob(job.id);
|
|
104
|
+
ctx.ui.notify(`Cancelled "${job.name}".`, "info");
|
|
105
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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(title, rows.map(r => r.label));
|
|
43
|
+
if (!choice) return undefined;
|
|
44
|
+
return rows.find(r => r.label === choice)?.item;
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* viewer-keys.ts — Scroll key matchers for the conversation viewer.
|
|
3
|
+
*
|
|
4
|
+
* Resolves `tui.select.*` through the user's keybindings when pi provides a
|
|
5
|
+
* manager, falling back to the previous hardcoded keys otherwise. The viewer's
|
|
6
|
+
* k/j and shift+arrow aliases always work alongside whatever is bound.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type KeyId, matchesKey } from "@earendil-works/pi-tui";
|
|
10
|
+
|
|
11
|
+
/** The `tui.select.*` keybinding ids the viewer resolves. */
|
|
12
|
+
export type ViewerScrollKeybinding =
|
|
13
|
+
| "tui.select.up"
|
|
14
|
+
| "tui.select.down"
|
|
15
|
+
| "tui.select.pageUp"
|
|
16
|
+
| "tui.select.pageDown";
|
|
17
|
+
|
|
18
|
+
/** Structural subset of pi-tui's `KeybindingsManager` (which satisfies it). */
|
|
19
|
+
export interface ViewerKeybindings {
|
|
20
|
+
matches(data: string, keybinding: ViewerScrollKeybinding): boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ViewerKeys {
|
|
24
|
+
scrollUp(data: string): boolean;
|
|
25
|
+
scrollDown(data: string): boolean;
|
|
26
|
+
pageUp(data: string): boolean;
|
|
27
|
+
pageDown(data: string): boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createViewerKeys(keybindings?: ViewerKeybindings): ViewerKeys {
|
|
31
|
+
const matches = (data: string, id: ViewerScrollKeybinding, fallback: KeyId): boolean =>
|
|
32
|
+
keybindings ? keybindings.matches(data, id) : matchesKey(data, fallback);
|
|
33
|
+
return {
|
|
34
|
+
scrollUp: (data) => matches(data, "tui.select.up", "up") || matchesKey(data, "k"),
|
|
35
|
+
scrollDown: (data) => matches(data, "tui.select.down", "down") || matchesKey(data, "j"),
|
|
36
|
+
pageUp: (data) => matches(data, "tui.select.pageUp", "pageUp") || matchesKey(data, "shift+up"),
|
|
37
|
+
pageDown: (data) => matches(data, "tui.select.pageDown", "pageDown") || matchesKey(data, "shift+down"),
|
|
38
|
+
};
|
|
39
|
+
}
|