@nredd/pi-subagents 0.0.0-stage → 0.22.0-nredd.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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1244 -2
  3. package/package.json +75 -4
  4. package/src/abortable.ts +43 -0
  5. package/src/agent-color.ts +161 -0
  6. package/src/agent-file-toggle.ts +297 -0
  7. package/src/agent-manager.ts +1874 -0
  8. package/src/agent-runner.ts +1552 -0
  9. package/src/agent-source-badge.ts +75 -0
  10. package/src/agent-types.ts +346 -0
  11. package/src/child-context.ts +15 -0
  12. package/src/clock.ts +25 -0
  13. package/src/completion-outbox.ts +125 -0
  14. package/src/context.ts +58 -0
  15. package/src/cross-extension-rpc.ts +221 -0
  16. package/src/custom-agents.ts +436 -0
  17. package/src/default-agents.ts +126 -0
  18. package/src/detached-results.ts +134 -0
  19. package/src/enabled-models.ts +180 -0
  20. package/src/enterprise-local.ts +66 -0
  21. package/src/env.ts +33 -0
  22. package/src/group-join.ts +148 -0
  23. package/src/index.ts +4423 -0
  24. package/src/invocation-config.ts +189 -0
  25. package/src/memory.ts +179 -0
  26. package/src/mention-clone.ts +195 -0
  27. package/src/mention.ts +141 -0
  28. package/src/model-call.ts +83 -0
  29. package/src/model-resolver.ts +266 -0
  30. package/src/model-scope.ts +70 -0
  31. package/src/nested-tools.ts +448 -0
  32. package/src/output-file.ts +164 -0
  33. package/src/package-resources.ts +624 -0
  34. package/src/prompts.ts +156 -0
  35. package/src/quota-admission.ts +99 -0
  36. package/src/schedule-store.ts +153 -0
  37. package/src/schedule.ts +446 -0
  38. package/src/settings.ts +728 -0
  39. package/src/skill-loader.ts +137 -0
  40. package/src/startup-error.ts +29 -0
  41. package/src/status-note.ts +90 -0
  42. package/src/structured-output.ts +130 -0
  43. package/src/types.ts +404 -0
  44. package/src/ui/agent-mention.ts +216 -0
  45. package/src/ui/agent-row.ts +235 -0
  46. package/src/ui/agent-widget.ts +692 -0
  47. package/src/ui/conversation-viewer.ts +832 -0
  48. package/src/ui/fleet-list.ts +675 -0
  49. package/src/ui/notification-view.ts +120 -0
  50. package/src/ui/schedule-menu.ts +105 -0
  51. package/src/ui/select-item.ts +45 -0
  52. package/src/ui/viewer-keys.ts +39 -0
  53. package/src/ui/workflow-card.ts +462 -0
  54. package/src/ui/workflow-dialog.ts +1115 -0
  55. package/src/ui/workflow-menu.ts +193 -0
  56. package/src/usage.ts +167 -0
  57. package/src/workflow/collisions.ts +123 -0
  58. package/src/workflow/entry.ts +47 -0
  59. package/src/workflow/host.ts +401 -0
  60. package/src/workflow/journal.ts +164 -0
  61. package/src/workflow/json-schema.ts +129 -0
  62. package/src/workflow/meta.ts +326 -0
  63. package/src/workflow/progress.ts +549 -0
  64. package/src/workflow/runtime.ts +1216 -0
  65. package/src/workflow/saved.ts +259 -0
  66. package/src/workflow/task.ts +302 -0
  67. package/src/workflow/tool-description.ts +200 -0
  68. package/src/workflow/worker-source.ts +781 -0
  69. package/src/worktree.ts +230 -0
  70. package/src/xml.ts +13 -0
@@ -0,0 +1,235 @@
1
+ /**
2
+ * agent-row.ts -- the `Agent` tool row.
3
+ *
4
+ * Core folds every collapsed tool row to one line,
5
+ * `<first line of renderCall> · <first line of renderResult>`, and draws the
6
+ * ▸/▾ disclosure marker itself. So the call's first line is just `Plan desc`,
7
+ * the result's first line is the summary (`thinking · 2m41s · 12s · 3.2k token`),
8
+ * and neither draws a marker of its own. The result is a live component: it
9
+ * reads the agent record on every render, so the summary keeps moving without
10
+ * the tool re-emitting a result.
11
+ */
12
+
13
+ import { pathToFileURL } from "node:url";
14
+ import { type Component, hyperlink, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
15
+ import { type Clock, realClock, type TimerHandle } from "../clock.js";
16
+ import { formatClock } from "../model-call.js";
17
+ import type { AgentRecord } from "../types.js";
18
+ import { getLifetimeCost, getLifetimeTotal } from "../usage.js";
19
+ import {
20
+ type AgentActivity,
21
+ type AgentDetails,
22
+ describeActivity,
23
+ fgPreservingNestedStyles,
24
+ formatCost,
25
+ formatTokens,
26
+ SPINNER,
27
+ type Theme,
28
+ } from "./agent-widget.js";
29
+
30
+ /** Lines of the result shown when expanded; the rest is one `get_subagent_result` away. */
31
+ const EXPANDED_RESULT_LINES = 50;
32
+ /** Lines of the prompt shown when expanded. */
33
+ const EXPANDED_PROMPT_LINES = 30;
34
+
35
+ /** What the row reads at render time. */
36
+ export interface AgentRowLive {
37
+ getRecord(id: string): AgentRecord | undefined;
38
+ getActivity(id: string): AgentActivity | undefined;
39
+ showCost(): boolean;
40
+ }
41
+
42
+ /** Statuses after which nothing about the run changes any more. */
43
+ const TERMINAL = new Set(["completed", "steered", "aborted", "stopped", "error"]);
44
+
45
+ /** Whether the row still has something live to show. */
46
+ export function isRowLive(details: AgentDetails, live: AgentRowLive): boolean {
47
+ const record = details.agentId ? live.getRecord(details.agentId) : undefined;
48
+ if (record) return record.status === "running" || record.status === "queued";
49
+ return details.status === "running" || details.status === "queued";
50
+ }
51
+
52
+ /** The icon and wording for a run's state, reading the record when the details are not final. */
53
+ function statusOf(
54
+ details: AgentDetails,
55
+ record: AgentRecord | undefined,
56
+ activity: AgentActivity | undefined,
57
+ theme: Theme,
58
+ now: number,
59
+ ): { icon: string; text: string } {
60
+ // A terminal result is the tool's own verdict (it also knows the worktree note
61
+ // and the final error). Only a non-final one is replaced by the live record.
62
+ const status = TERMINAL.has(details.status) ? details.status : (record?.status ?? details.status);
63
+ switch (status) {
64
+ case "completed":
65
+ return { icon: theme.fg("success", "✓"), text: "done" };
66
+ case "steered":
67
+ return { icon: theme.fg("warning", "✓"), text: "wrapped up (turn limit)" };
68
+ case "stopped":
69
+ return { icon: theme.fg("dim", "■"), text: "stopped" };
70
+ case "aborted":
71
+ return { icon: theme.fg("error", "✗"), text: "aborted (max turns exceeded)" };
72
+ case "error":
73
+ return { icon: theme.fg("error", "✗"), text: `error: ${record?.error ?? details.error ?? "unknown"}` };
74
+ case "queued":
75
+ return { icon: theme.fg("muted", "◦"), text: details.activity ?? "queued" };
76
+ default: {
77
+ const frame = SPINNER[Math.floor(now / 80) % SPINNER.length];
78
+ const text = activity
79
+ ? describeActivity(activity.activeTools, activity.responseText, activity.modelCall, now)
80
+ : (details.status === "background" ? "running in background" : (details.activity ?? "thinking…"));
81
+ return { icon: theme.fg("accent", frame), text };
82
+ }
83
+ }
84
+ }
85
+
86
+ /** `✓ done · 12s · 33.8k token`: the first line of the result, and half of the folded row. */
87
+ export function agentRowSummary(
88
+ details: AgentDetails,
89
+ live: AgentRowLive,
90
+ theme: Theme,
91
+ now: number = Date.now(),
92
+ ): string {
93
+ const record = details.agentId ? live.getRecord(details.agentId) : undefined;
94
+ const activity = details.agentId ? live.getActivity(details.agentId) : undefined;
95
+ const { icon, text } = statusOf(details, record, activity, theme, now);
96
+
97
+ const finished = TERMINAL.has(details.status) || (record !== undefined && TERMINAL.has(record.status));
98
+ const elapsed = record
99
+ ? (finished ? (record.completedAt ?? now) : now) - record.startedAt
100
+ : details.durationMs;
101
+ const tokens = record ? getLifetimeTotal(record.lifetimeUsage) : 0;
102
+ const tokenText = record ? (tokens > 0 ? formatTokens(tokens) : "") : details.tokens;
103
+ const cost = live.showCost() ? formatCost(record ? getLifetimeCost(record.lifetimeUsage) : (details.cost ?? 0)) : "";
104
+
105
+ const parts = [text, formatClock(elapsed)];
106
+ if (tokenText) parts.push(tokenText);
107
+ if (cost) parts.push(cost);
108
+ if (!finished && details.backgroundHint && elapsed >= 2000) parts.push("(ctrl+b to run in background)");
109
+ return `${icon} ${parts.map(p => fgPreservingNestedStyles(theme, "dim", p)).join(` ${theme.fg("dim", "·")} `)}`;
110
+ }
111
+
112
+ /**
113
+ * `↳ opus 4.6 · thinking: high · background`. Taken from the details, which the
114
+ * tool already builds from the record's effective values once a session exists
115
+ * (pi may have clamped the level) and which carry the `twin` label.
116
+ */
117
+ function tagsLine(details: AgentDetails, theme: Theme): string | undefined {
118
+ const parts = [...(details.modelName ? [details.modelName] : []), ...(details.tags ?? [])];
119
+ return parts.length > 0 ? theme.fg("dim", `↳ ${parts.join(" · ")}`) : undefined;
120
+ }
121
+
122
+ /** The transcript as an OSC 8 hyperlink, so a click opens it. */
123
+ export function transcriptLink(path: string, theme: Theme): string {
124
+ return `${hyperlink("transcript ↗", pathToFileURL(path).href)} ${theme.fg("dim", path)}`;
125
+ }
126
+
127
+ /** Redraw cadence while a row has something live to show. */
128
+ const ROW_TICK_MS = 1000;
129
+
130
+ interface RowTickState {
131
+ ticker?: TimerHandle;
132
+ isLive?: () => boolean;
133
+ invalidate?: () => void;
134
+ }
135
+
136
+ /**
137
+ * Ask the host to redraw a live tool row once a second. Pi caches a settled
138
+ * row's collapsed line until the renderer calls `context.invalidate()`, and
139
+ * nothing repaints a quiet screen, so a row that reads live state must tick.
140
+ * The ticker lives on the row's shared `state`, always asks the newest
141
+ * `isLive`, and stops by itself once the run is over. A host probe render
142
+ * (`probe: true`) gets throwaway state and re-runs on every invalidate, so it
143
+ * never starts one.
144
+ */
145
+ export function keepTicking(
146
+ context: { state?: unknown; invalidate?: () => void; probe?: boolean },
147
+ isLive: () => boolean,
148
+ clock: Clock = realClock,
149
+ ): void {
150
+ const state = context.state as RowTickState | undefined;
151
+ if (context.probe || !state || !context.invalidate) return;
152
+ state.isLive = isLive;
153
+ state.invalidate = context.invalidate;
154
+ if (state.ticker !== undefined || !isLive()) return;
155
+ state.ticker = clock.setInterval(() => {
156
+ if (state.isLive?.()) {
157
+ state.invalidate?.();
158
+ return;
159
+ }
160
+ state.ticker?.cancel();
161
+ state.ticker = undefined;
162
+ state.invalidate?.(); // one last repaint, showing the final state
163
+ }, ROW_TICK_MS);
164
+ state.ticker.unref?.();
165
+ }
166
+
167
+ /** {@link keepTicking} for an `Agent` row: live while its record is running or queued. */
168
+ export function keepRowTicking(
169
+ context: { state?: unknown; invalidate?: () => void; probe?: boolean },
170
+ details: AgentDetails,
171
+ live: AgentRowLive,
172
+ clock: Clock = realClock,
173
+ ): void {
174
+ keepTicking(context, () => isRowLive(details, live), clock);
175
+ }
176
+
177
+ /**
178
+ * The result half of the row. Re-evaluates on every render, so what it shows is
179
+ * the agent's state now rather than the state when the tool last returned.
180
+ */
181
+ export class AgentResultView implements Component {
182
+ constructor(
183
+ private readonly details: AgentDetails,
184
+ private readonly resultText: string,
185
+ private readonly expanded: boolean,
186
+ private readonly live: AgentRowLive,
187
+ private readonly theme: Theme,
188
+ private readonly clock: Clock = realClock,
189
+ ) {}
190
+
191
+ render(width: number): string[] {
192
+ const lines = [truncateToWidth(agentRowSummary(this.details, this.live, this.theme, this.clock.now()), width)];
193
+ if (!this.expanded) return lines;
194
+
195
+ const record = this.details.agentId ? this.live.getRecord(this.details.agentId) : undefined;
196
+ const tags = tagsLine(this.details, this.theme);
197
+ if (tags) lines.push(truncateToWidth(tags, width));
198
+ const transcript = record?.outputFile ?? this.details.outputFile;
199
+ if (transcript) lines.push(truncateToWidth(transcriptLink(transcript, this.theme), width));
200
+
201
+ if (this.details.status === "completed" || this.details.status === "steered") {
202
+ const body = this.resultText.split("\n");
203
+ for (const line of body.slice(0, EXPANDED_RESULT_LINES)) {
204
+ lines.push(...wrapTextWithAnsi(this.theme.fg("dim", ` ${line}`), width));
205
+ }
206
+ if (body.length > EXPANDED_RESULT_LINES) {
207
+ lines.push(this.theme.fg("muted", " ... (use get_subagent_result with verbose for full output)"));
208
+ }
209
+ }
210
+ return lines;
211
+ }
212
+
213
+ invalidate(): void { /* reads live state on every render */ }
214
+ }
215
+
216
+ /** The call half: `Plan desc`, and the prompt underneath when expanded. */
217
+ export class AgentCallView implements Component {
218
+ constructor(
219
+ private readonly header: string,
220
+ private readonly prompt: string | undefined,
221
+ private readonly theme: Theme,
222
+ ) {}
223
+
224
+ render(width: number): string[] {
225
+ const lines = [truncateToWidth(this.header, width)];
226
+ if (this.prompt) {
227
+ const wrapped = this.prompt.split("\n").flatMap(line => wrapTextWithAnsi(this.theme.fg("dim", ` ${line}`), width));
228
+ lines.push(...wrapped.slice(0, EXPANDED_PROMPT_LINES));
229
+ if (wrapped.length > EXPANDED_PROMPT_LINES) lines.push(this.theme.fg("muted", " …"));
230
+ }
231
+ return lines;
232
+ }
233
+
234
+ invalidate(): void { /* static */ }
235
+ }