@clanker-code/pi-subagents 0.10.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.
Files changed (130) hide show
  1. package/.plans/PLAN-next-changes.md +183 -0
  2. package/.plans/README.md +14 -0
  3. package/AGENTS.md +31 -0
  4. package/CHANGELOG.md +583 -0
  5. package/CLAUDE.md +1 -0
  6. package/LICENSE +21 -0
  7. package/README.md +630 -0
  8. package/RELEASE.md +39 -0
  9. package/dist/abort-resend.d.ts +35 -0
  10. package/dist/abort-resend.js +71 -0
  11. package/dist/agent-details.d.ts +17 -0
  12. package/dist/agent-details.js +22 -0
  13. package/dist/agent-manager.d.ts +132 -0
  14. package/dist/agent-manager.js +493 -0
  15. package/dist/agent-runner.d.ts +165 -0
  16. package/dist/agent-runner.js +732 -0
  17. package/dist/agent-tool-description.d.ts +9 -0
  18. package/dist/agent-tool-description.js +147 -0
  19. package/dist/agent-types.d.ts +60 -0
  20. package/dist/agent-types.js +157 -0
  21. package/dist/context.d.ts +12 -0
  22. package/dist/context.js +56 -0
  23. package/dist/cross-extension-rpc.d.ts +46 -0
  24. package/dist/cross-extension-rpc.js +76 -0
  25. package/dist/custom-agents.d.ts +14 -0
  26. package/dist/custom-agents.js +149 -0
  27. package/dist/default-agents.d.ts +7 -0
  28. package/dist/default-agents.js +119 -0
  29. package/dist/enabled-models.d.ts +49 -0
  30. package/dist/enabled-models.js +145 -0
  31. package/dist/env.d.ts +6 -0
  32. package/dist/env.js +28 -0
  33. package/dist/group-join.d.ts +32 -0
  34. package/dist/group-join.js +116 -0
  35. package/dist/index.d.ts +36 -0
  36. package/dist/index.js +1918 -0
  37. package/dist/invocation-config.d.ts +25 -0
  38. package/dist/invocation-config.js +19 -0
  39. package/dist/memory.d.ts +49 -0
  40. package/dist/memory.js +151 -0
  41. package/dist/model-resolver.d.ts +19 -0
  42. package/dist/model-resolver.js +62 -0
  43. package/dist/notifications.d.ts +6 -0
  44. package/dist/notifications.js +107 -0
  45. package/dist/output-file.d.ts +24 -0
  46. package/dist/output-file.js +86 -0
  47. package/dist/peek.d.ts +37 -0
  48. package/dist/peek.js +121 -0
  49. package/dist/prompts.d.ts +40 -0
  50. package/dist/prompts.js +95 -0
  51. package/dist/schedule-store.d.ts +38 -0
  52. package/dist/schedule-store.js +155 -0
  53. package/dist/schedule.d.ts +109 -0
  54. package/dist/schedule.js +338 -0
  55. package/dist/settings.d.ts +135 -0
  56. package/dist/settings.js +168 -0
  57. package/dist/skill-loader.d.ts +24 -0
  58. package/dist/skill-loader.js +93 -0
  59. package/dist/status-note.d.ts +13 -0
  60. package/dist/status-note.js +24 -0
  61. package/dist/types.d.ts +184 -0
  62. package/dist/types.js +7 -0
  63. package/dist/ui/agent-tool-rendering.d.ts +34 -0
  64. package/dist/ui/agent-tool-rendering.js +154 -0
  65. package/dist/ui/agent-widget-tree.d.ts +33 -0
  66. package/dist/ui/agent-widget-tree.js +130 -0
  67. package/dist/ui/agent-widget.d.ts +156 -0
  68. package/dist/ui/agent-widget.js +408 -0
  69. package/dist/ui/conversation-viewer.d.ts +47 -0
  70. package/dist/ui/conversation-viewer.js +290 -0
  71. package/dist/ui/menu-select.d.ts +20 -0
  72. package/dist/ui/menu-select.js +46 -0
  73. package/dist/ui/schedule-menu.d.ts +16 -0
  74. package/dist/ui/schedule-menu.js +99 -0
  75. package/dist/ui/viewer-keys.d.ts +20 -0
  76. package/dist/ui/viewer-keys.js +17 -0
  77. package/dist/usage.d.ts +50 -0
  78. package/dist/usage.js +49 -0
  79. package/dist/wait.d.ts +10 -0
  80. package/dist/wait.js +37 -0
  81. package/dist/worktree.d.ts +45 -0
  82. package/dist/worktree.js +160 -0
  83. package/docs/design/default-extension-tool-exposure.md +56 -0
  84. package/docs/superpowers/plans/2026-06-19-recursive-subagent-widget.md +600 -0
  85. package/docs/superpowers/specs/2026-06-19-recursive-subagent-widget-design.md +189 -0
  86. package/examples/agent-tool-description.md +45 -0
  87. package/package.json +56 -0
  88. package/reviews/proposal-structured-output-schema.md +135 -0
  89. package/reviews/recursive-subagent-widget-preview-rev2.png +0 -0
  90. package/reviews/recursive-subagent-widget-preview.html +137 -0
  91. package/reviews/recursive-subagent-widget-preview.png +0 -0
  92. package/reviews/subagent-features-comparison.md +350 -0
  93. package/src/abort-resend.ts +75 -0
  94. package/src/agent-details.ts +31 -0
  95. package/src/agent-manager.ts +596 -0
  96. package/src/agent-runner.ts +872 -0
  97. package/src/agent-tool-description.ts +163 -0
  98. package/src/agent-types.ts +189 -0
  99. package/src/context.ts +58 -0
  100. package/src/cross-extension-rpc.ts +122 -0
  101. package/src/custom-agents.ts +160 -0
  102. package/src/default-agents.ts +123 -0
  103. package/src/enabled-models.ts +180 -0
  104. package/src/env.ts +33 -0
  105. package/src/group-join.ts +141 -0
  106. package/src/index.ts +2115 -0
  107. package/src/invocation-config.ts +42 -0
  108. package/src/memory.ts +165 -0
  109. package/src/model-resolver.ts +81 -0
  110. package/src/notifications.ts +120 -0
  111. package/src/output-file.ts +96 -0
  112. package/src/peek.ts +155 -0
  113. package/src/prompts.ts +129 -0
  114. package/src/schedule-store.ts +153 -0
  115. package/src/schedule.ts +365 -0
  116. package/src/settings.ts +289 -0
  117. package/src/skill-loader.ts +102 -0
  118. package/src/status-note.ts +25 -0
  119. package/src/types.ts +195 -0
  120. package/src/ui/agent-tool-rendering.ts +175 -0
  121. package/src/ui/agent-widget-tree.ts +169 -0
  122. package/src/ui/agent-widget.ts +497 -0
  123. package/src/ui/conversation-viewer.ts +297 -0
  124. package/src/ui/menu-select.ts +68 -0
  125. package/src/ui/schedule-menu.ts +105 -0
  126. package/src/ui/viewer-keys.ts +39 -0
  127. package/src/usage.ts +60 -0
  128. package/src/wait.ts +44 -0
  129. package/src/worktree.ts +191 -0
  130. package/vitest.config.ts +25 -0
@@ -0,0 +1,175 @@
1
+ import { Text } from "@earendil-works/pi-tui";
2
+ import { getModelLabelFromConfig } from "../agent-tool-description.js";
3
+ import { getAgentConfig } from "../agent-types.js";
4
+ import { extractText } from "../context.js";
5
+ import { type AgentDetails, formatMs, formatTurns, getDisplayName, SPINNER } from "./agent-widget.js";
6
+
7
+ export function compactPreview(text: string, maxLen = 80): string {
8
+ const oneLine = text.replace(/\s+/g, " ").trim();
9
+ if (oneLine.length <= maxLen) return oneLine;
10
+ return oneLine.slice(0, maxLen - 1).trimEnd() + "…";
11
+ }
12
+
13
+ export function tailPreview(text: string, maxLen = 100): string {
14
+ const oneLine = text.replace(/\s+/g, " ").trim();
15
+ if (oneLine.length <= maxLen) return oneLine;
16
+ return "…" + oneLine.slice(oneLine.length - (maxLen - 1));
17
+ }
18
+
19
+ export function snipMiddleLines(text: string, edgeLines = 20): string[] {
20
+ const lines = text.split("\n");
21
+ const maxLines = edgeLines * 2;
22
+ if (lines.length <= maxLines) return lines;
23
+ const omitted = lines.length - maxLines;
24
+ return [
25
+ ...lines.slice(0, edgeLines),
26
+ `... ${omitted} lines omitted; expand for full output ...`,
27
+ ...lines.slice(-edgeLines),
28
+ ];
29
+ }
30
+
31
+ /**
32
+ * Shape of the args passed to the Agent tool's `renderCall`. Mirrors the tool
33
+ * parameter schema — only the fields whose presence changes the call-time
34
+ * header are listed. Anything we can't decide at call time (e.g. an agent that
35
+ * inherits its model from the parent) is intentionally omitted here and is
36
+ * surfaced later by `renderResult`.
37
+ */
38
+ export interface AgentCallArgs {
39
+ subagent_type?: string;
40
+ description?: string;
41
+ /** Explicit per-call model override (`provider/modelId` or fuzzy name). */
42
+ model?: string;
43
+ /** Agent ID being resumed; only set on resume calls. */
44
+ resume?: string;
45
+ /** Schedule expression; only set on schedule calls. */
46
+ schedule?: string;
47
+ /** Drop parent extension tools for this run. */
48
+ isolated?: boolean;
49
+ /** Isolation mode — currently only "worktree". */
50
+ isolation?: "worktree";
51
+ }
52
+
53
+ /**
54
+ * Resolve the model that this call will use, if it's knowable before execute().
55
+ * Resolution order: explicit `args.model` > agent config frontmatter > none.
56
+ * Inheritance from the parent session is decided inside execute() and surfaced
57
+ * by `renderResult` once the resolved model is available.
58
+ */
59
+ function resolveCallModel(args: any): string | undefined {
60
+ if (typeof args.model === "string" && args.model) return args.model;
61
+ if (typeof args.subagent_type === "string") return getAgentConfig(args.subagent_type)?.model;
62
+ return undefined;
63
+ }
64
+
65
+ /** Build the dimmed badge list shown between the agent name and the description. */
66
+ function buildCallBadges(args: any, theme: any): string {
67
+ const badges: string[] = [];
68
+ const callModel = resolveCallModel(args);
69
+ if (callModel) badges.push(getModelLabelFromConfig(callModel));
70
+ if (typeof args.resume === "string" && args.resume) {
71
+ badges.push(`resume: ${compactPreview(args.resume, 12)}`);
72
+ }
73
+ if (typeof args.schedule === "string" && args.schedule) {
74
+ badges.push(`schedule: ${compactPreview(args.schedule, 20)}`);
75
+ }
76
+ if (args.isolation === "worktree") badges.push("worktree");
77
+ if (args.isolated) badges.push("isolated");
78
+ if (badges.length === 0) return "";
79
+ const sep = " " + theme.fg("dim", "·") + " ";
80
+ return " " + badges.map((b) => theme.fg("dim", b)).join(sep);
81
+ }
82
+
83
+ export function renderAgentCall(args: any, theme: any): Text {
84
+ const displayName = args.subagent_type ? getDisplayName(args.subagent_type) : "Agent";
85
+ const desc = args.description ?? "";
86
+ const badges = buildCallBadges(args, theme);
87
+ const descPart = desc ? " " + theme.fg("muted", desc) : "";
88
+ return new Text("▸ " + theme.fg("toolTitle", theme.bold(displayName)) + badges + descPart, 0, 0);
89
+ }
90
+
91
+ function getResultText(content: unknown): string {
92
+ if (!Array.isArray(content)) return typeof content === "string" ? content : "";
93
+ return extractText(content);
94
+ }
95
+
96
+ export function renderAgentResult(result: any, { expanded, isPartial }: { expanded: boolean; isPartial: boolean }, theme: any): Text {
97
+ const details = result.details as AgentDetails | undefined;
98
+ if (!details) {
99
+ return new Text(getResultText(result.content), 0, 0);
100
+ }
101
+
102
+ const stats = (d: AgentDetails) => {
103
+ const parts: string[] = [];
104
+ if (d.modelName) parts.push(d.modelName);
105
+ if (d.tags) parts.push(...d.tags);
106
+ if (d.turnCount != null && d.turnCount > 0) {
107
+ parts.push(formatTurns(d.turnCount, d.maxTurns));
108
+ }
109
+ if (d.toolUses > 0) parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
110
+ if (d.tokens) parts.push(d.tokens);
111
+ return parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
112
+ };
113
+
114
+ if (isPartial || details.status === "running") {
115
+ const frame = SPINNER[details.spinnerFrame ?? 0];
116
+ const s = stats(details);
117
+ let line = theme.fg("accent", frame) + (s ? " " + s : "");
118
+ line += "\n" + theme.fg("dim", ` ⎿ ${details.activity ?? "thinking…"}`);
119
+ return new Text(line, 0, 0);
120
+ }
121
+
122
+ if (details.status === "background") {
123
+ return new Text(theme.fg("dim", ` ⎿ Running in background (ID: ${details.agentId})`), 0, 0);
124
+ }
125
+
126
+ if (details.status === "completed" || details.status === "steered") {
127
+ const duration = formatMs(details.durationMs);
128
+ const isSteered = details.status === "steered";
129
+ const icon = isSteered ? theme.fg("warning", "✓") : theme.fg("success", "✓");
130
+ const s = stats(details);
131
+ let line = icon + (s ? " " + s : "");
132
+ line += " " + theme.fg("dim", "·") + " " + theme.fg("dim", duration);
133
+
134
+ const resultText = getResultText(result.content).trim();
135
+ if (expanded) {
136
+ if (resultText) {
137
+ for (const lineText of resultText.split("\n")) {
138
+ line += "\n" + theme.fg("dim", ` ${lineText}`);
139
+ }
140
+ }
141
+ } else {
142
+ const doneText = isSteered ? "Wrapped up (turn limit)" : "Done";
143
+ if (resultText) {
144
+ for (const lineText of snipMiddleLines(resultText)) {
145
+ line += "\n" + theme.fg("dim", ` ${lineText}`);
146
+ }
147
+ } else {
148
+ line += "\n" + theme.fg("dim", ` ⎿ ${doneText}`);
149
+ }
150
+ }
151
+ return new Text(line, 0, 0);
152
+ }
153
+
154
+ if (details.status === "stopped") {
155
+ const s = stats(details);
156
+ let line = theme.fg("dim", "■") + (s ? " " + s : "");
157
+ line += "\n" + theme.fg("dim", " ⎿ Stopped");
158
+ return new Text(line, 0, 0);
159
+ }
160
+
161
+ const s = stats(details);
162
+ let line = theme.fg("error", "✗") + (s ? " " + s : "");
163
+ if (details.status === "error") {
164
+ line += "\n" + theme.fg("error", ` ⎿ Error: ${details.error ?? "unknown"}`);
165
+ } else {
166
+ line += "\n" + theme.fg("warning", " ⎿ Aborted (max turns exceeded)");
167
+ }
168
+ return new Text(line, 0, 0);
169
+ }
170
+
171
+ export function renderSteerCall(args: { agent_id?: string; message?: string }, theme: any): Text {
172
+ const agentId = args.agent_id ? ` ${theme.fg("muted", args.agent_id)}` : "";
173
+ const preview = args.message ? ` ${theme.fg("dim", `“${compactPreview(args.message)}”`)}` : "";
174
+ return new Text(`▸ ${theme.fg("toolTitle", theme.bold("Steer Agent"))}${agentId}${preview}`, 0, 0);
175
+ }
@@ -0,0 +1,169 @@
1
+ import { truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { getConfig } from "../agent-types.js";
3
+ import type { AgentInvocation, SubagentType } from "../types.js";
4
+ import type { AgentActivity, Theme } from "./agent-widget.js";
5
+
6
+ export type WidgetDisplayMode = "auto" | "rich" | "compact";
7
+
8
+ export interface WidgetAgentSnapshot {
9
+ id: string;
10
+ parentAgentId?: string;
11
+ depth?: number;
12
+ type: SubagentType;
13
+ description: string;
14
+ status: string;
15
+ startedAt: number;
16
+ completedAt?: number;
17
+ error?: string;
18
+ toolUses: number;
19
+ invocation?: AgentInvocation;
20
+ activity?: AgentActivity;
21
+ }
22
+
23
+ export interface WidgetTreeNode {
24
+ snapshot: WidgetAgentSnapshot;
25
+ children: WidgetTreeNode[];
26
+ orphaned?: boolean;
27
+ }
28
+
29
+ function statusRank(status: string): number {
30
+ if (status === "running") return 0;
31
+ if (status === "queued") return 1;
32
+ return 2;
33
+ }
34
+
35
+ function sortNodes(a: WidgetTreeNode, b: WidgetTreeNode): number {
36
+ const status = statusRank(a.snapshot.status) - statusRank(b.snapshot.status);
37
+ if (status !== 0) return status;
38
+ return a.snapshot.startedAt - b.snapshot.startedAt;
39
+ }
40
+
41
+ export interface RenderTreeOptions {
42
+ mode: WidgetDisplayMode;
43
+ width: number;
44
+ maxLines: number;
45
+ theme: Theme;
46
+ frame: string;
47
+ now?: number;
48
+ }
49
+
50
+ export function buildAgentTree(records: WidgetAgentSnapshot[]): WidgetTreeNode[] {
51
+ const nodes = new Map<string, WidgetTreeNode>();
52
+ for (const record of records) nodes.set(record.id, { snapshot: record, children: [] });
53
+
54
+ const roots: WidgetTreeNode[] = [];
55
+ for (const node of nodes.values()) {
56
+ const parentId = node.snapshot.parentAgentId;
57
+ const parent = parentId ? nodes.get(parentId) : undefined;
58
+ if (parent) {
59
+ parent.children.push(node);
60
+ } else {
61
+ if (parentId) node.orphaned = true;
62
+ roots.push(node);
63
+ }
64
+ }
65
+
66
+ function sortDeep(items: WidgetTreeNode[]) {
67
+ items.sort(sortNodes);
68
+ for (const item of items) sortDeep(item.children);
69
+ }
70
+
71
+ sortDeep(roots);
72
+ return roots;
73
+ }
74
+
75
+ export function chooseEffectiveMode(
76
+ mode: WidgetDisplayMode,
77
+ width: number,
78
+ richLineCount: number,
79
+ maxLines: number,
80
+ ): "rich" | "compact" {
81
+ if (mode === "rich" || mode === "compact") return mode;
82
+ if (width < 88) return "compact";
83
+ if (richLineCount > maxLines) return "compact";
84
+ return "rich";
85
+ }
86
+
87
+ function formatElapsed(startedAt: number, now: number): string {
88
+ const seconds = Math.max(0, Math.floor((now - startedAt) / 1000));
89
+ if (seconds < 60) return `${seconds}s`;
90
+ const minutes = Math.floor(seconds / 60);
91
+ const rest = seconds % 60;
92
+ return rest > 0 ? `${minutes}m ${rest}s` : `${minutes}m`;
93
+ }
94
+
95
+ function statusIcon(snapshot: WidgetAgentSnapshot, frame: string, theme: Theme): string {
96
+ if (snapshot.status === "running") return theme.fg("accent", frame);
97
+ if (snapshot.status === "queued") return theme.fg("muted", "◦");
98
+ if (snapshot.status === "completed") return theme.fg("success", "✓");
99
+ if (snapshot.status === "stopped") return theme.fg("dim", "■");
100
+ return theme.fg("error", "✗");
101
+ }
102
+
103
+ function displayName(type: SubagentType): string {
104
+ return getConfig(type).displayName;
105
+ }
106
+
107
+ function collectRows(
108
+ nodes: WidgetTreeNode[],
109
+ options: RenderTreeOptions,
110
+ mode: "rich" | "compact",
111
+ prefix = "",
112
+ ): string[] {
113
+ const rows: string[] = [];
114
+ nodes.forEach((node, index) => {
115
+ const isLast = index === nodes.length - 1;
116
+ const connector = isLast ? "└─" : "├─";
117
+ const childPrefix = prefix + (isLast ? " " : "│ ");
118
+ const s = node.snapshot;
119
+ const name = displayName(s.type);
120
+ const elapsed = formatElapsed(s.startedAt, options.now ?? Date.now());
121
+ const stats: string[] = [];
122
+ if (s.activity?.turnCount) stats.push(`↻${s.activity.turnCount}`);
123
+ if (s.toolUses > 0) stats.push(`${s.toolUses} tool${s.toolUses === 1 ? "" : "s"}`);
124
+ stats.push(elapsed);
125
+ const orphan = node.orphaned ? " ⚠ orphan" : "";
126
+ const error = s.error ? ` error: ${s.error}` : "";
127
+ rows.push(`${prefix}${connector} ${statusIcon(s, options.frame, options.theme)} ${options.theme.bold(name)} ${options.theme.fg("muted", s.description)} ${options.theme.fg("dim", `· ${stats.join(" · ")}${orphan}${error}`)}`);
128
+
129
+ if (mode === "rich" && s.status === "running") {
130
+ const activity = s.activity?.activityDescription ?? "thinking…";
131
+ rows.push(`${childPrefix}${options.theme.fg("dim", `⎿ ${activity}`)}`);
132
+ }
133
+
134
+ rows.push(...collectRows(node.children, options, mode, childPrefix));
135
+ });
136
+ return rows;
137
+ }
138
+
139
+ function applyOverflow(lines: string[], maxLines: number, width: number, hiddenLabel = "agents"): string[] {
140
+ if (lines.length <= maxLines) return lines.map(line => truncateToWidth(line, width));
141
+ if (maxLines <= 1) return [truncateToWidth(`+${lines.length} more ${hiddenLabel} hidden`, width)];
142
+ const visible = lines.slice(0, maxLines - 1);
143
+ const hidden = lines.length - visible.length;
144
+ visible.push(`└─ +${hidden} more ${hiddenLabel} hidden`);
145
+ return visible.map(line => truncateToWidth(line, width));
146
+ }
147
+
148
+ export function renderAgentTree(records: WidgetAgentSnapshot[], options: RenderTreeOptions): string[] {
149
+ const tree = buildAgentTree(records);
150
+ const now = options.now ?? Date.now();
151
+ const active = records.filter(r => r.status === "running").length;
152
+ const queued = records.filter(r => r.status === "queued").length;
153
+ const maxDepth = records.reduce((max, r) => Math.max(max, r.depth ?? 0), 0);
154
+ let mode: "rich" | "compact";
155
+ let rows: string[];
156
+ if (options.mode === "rich" || options.mode === "compact") {
157
+ mode = options.mode;
158
+ rows = collectRows(tree, { ...options, now }, mode);
159
+ } else {
160
+ const richRows = collectRows(tree, { ...options, now }, "rich");
161
+ mode = chooseEffectiveMode(options.mode, options.width, richRows.length + 1, options.maxLines);
162
+ rows = mode === "rich" ? richRows : collectRows(tree, { ...options, now }, "compact");
163
+ }
164
+ const suffix = mode === "rich" && records.length > 0
165
+ ? options.theme.fg("dim", ` ${active} running · ${queued} queued · depth ${maxDepth}/4`)
166
+ : "";
167
+ const heading = `${active > 0 ? options.theme.fg("accent", "●") : options.theme.fg("dim", "○")} ${options.theme.fg(active > 0 ? "accent" : "dim", "Agents")}${suffix}`;
168
+ return applyOverflow([heading, ...rows], options.maxLines, options.width);
169
+ }