@yagni-app/code 0.2.1 → 0.3.1
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/dist/cli.d.ts +30 -0
- package/dist/cli.js +147 -3
- package/dist/connectClaudeCode.d.ts +77 -0
- package/dist/connectClaudeCode.js +228 -0
- package/dist/connectCodex.d.ts +75 -0
- package/dist/connectCodex.js +201 -0
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/extension/advisor.d.ts +4 -4
- package/dist/extension/advisor.js +6 -7
- package/dist/extension/approvedPrefixes.d.ts +103 -0
- package/dist/extension/approvedPrefixes.js +282 -0
- package/dist/extension/askAdvisorTool.d.ts +20 -5
- package/dist/extension/askAdvisorTool.js +126 -20
- package/dist/extension/askYagniTool.d.ts +23 -0
- package/dist/extension/askYagniTool.js +91 -2
- package/dist/extension/branding.d.ts +34 -3
- package/dist/extension/branding.js +114 -13
- package/dist/extension/chipEditor.d.ts +30 -9
- package/dist/extension/chipEditor.js +173 -59
- package/dist/extension/claudeRules.d.ts +0 -2
- package/dist/extension/claudeRules.js +0 -8
- package/dist/extension/cmux/dispatcher.d.ts +25 -0
- package/dist/extension/cmux/dispatcher.js +266 -0
- package/dist/extension/cmux/hooks.d.ts +12 -0
- package/dist/extension/cmux/hooks.js +192 -0
- package/dist/extension/cmux/index.d.ts +3 -0
- package/dist/extension/cmux/index.js +155 -0
- package/dist/extension/cmux/naming.d.ts +5 -0
- package/dist/extension/cmux/naming.js +23 -0
- package/dist/extension/cmux/state.d.ts +33 -0
- package/dist/extension/cmux/state.js +142 -0
- package/dist/extension/config.d.ts +44 -1
- package/dist/extension/config.js +37 -4
- package/dist/extension/costHud.d.ts +16 -22
- package/dist/extension/costHud.js +8 -47
- package/dist/extension/crashReport.js +1 -3
- package/dist/extension/execPolicy.d.ts +135 -0
- package/dist/extension/execPolicy.js +936 -0
- package/dist/extension/flywheel.d.ts +44 -0
- package/dist/extension/flywheel.js +53 -0
- package/dist/extension/footer.d.ts +118 -0
- package/dist/extension/footer.js +308 -0
- package/dist/extension/guardian.d.ts +139 -0
- package/dist/extension/guardian.js +237 -0
- package/dist/extension/index.d.ts +35 -7
- package/dist/extension/index.js +338 -33
- package/dist/extension/mineBeat.d.ts +95 -0
- package/dist/extension/mineBeat.js +193 -0
- package/dist/extension/permission.d.ts +124 -10
- package/dist/extension/permission.js +603 -52
- package/dist/extension/pipeline/childRegistry.d.ts +41 -0
- package/dist/extension/pipeline/childRegistry.js +118 -0
- package/dist/extension/pipeline/finish.js +5 -1
- package/dist/extension/pipeline/goCommand.d.ts +1 -1
- package/dist/extension/pipeline/goCommand.js +40 -9
- package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
- package/dist/extension/pipeline/goStatusCommands.js +61 -1
- package/dist/extension/pipeline/personas.js +26 -1
- package/dist/extension/pipeline/resilience.d.ts +2 -1
- package/dist/extension/pipeline/resilience.js +21 -2
- package/dist/extension/pipeline/runRegistry.d.ts +23 -1
- package/dist/extension/pipeline/runRegistry.js +57 -1
- package/dist/extension/pipeline/runner.js +4 -0
- package/dist/extension/pipeline/verify.d.ts +4 -0
- package/dist/extension/pipeline/verify.js +48 -26
- package/dist/extension/recordDecisionTool.d.ts +8 -0
- package/dist/extension/recordDecisionTool.js +24 -0
- package/dist/extension/redact.d.ts +20 -0
- package/dist/extension/redact.js +64 -0
- package/dist/extension/rerouteNotice.d.ts +3 -4
- package/dist/extension/rerouteNotice.js +20 -11
- package/dist/extension/subagentRender.d.ts +129 -0
- package/dist/extension/subagentRender.js +441 -0
- package/dist/extension/subagents.d.ts +11 -8
- package/dist/extension/subagents.js +162 -37
- package/dist/extension/ticketTools.d.ts +37 -0
- package/dist/extension/ticketTools.js +117 -0
- package/dist/extension/todos.d.ts +28 -1
- package/dist/extension/todos.js +76 -1
- package/dist/extension/tokenProvider.js +46 -5
- package/dist/extension/ultra.d.ts +27 -0
- package/dist/extension/ultra.js +76 -0
- package/dist/launch.d.ts +7 -0
- package/dist/launch.js +24 -12
- package/dist/login.d.ts +4 -2
- package/dist/login.js +19 -4
- package/dist/padding.d.ts +22 -0
- package/dist/padding.js +25 -0
- package/dist/promptEnrichment.d.ts +40 -0
- package/dist/promptEnrichment.js +85 -0
- package/dist/signalForward.d.ts +60 -0
- package/dist/signalForward.js +130 -0
- package/dist/token.d.ts +25 -0
- package/dist/token.js +45 -0
- package/package.json +6 -5
- package/dist/extension/boostCommand.d.ts +0 -144
- package/dist/extension/boostCommand.js +0 -263
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live progress model + TUI renderers for the `subagent` tool.
|
|
3
|
+
*
|
|
4
|
+
* The model half is PURE (mirrors `pipeline/activity.ts`): `applyChildEvent`
|
|
5
|
+
* folds one NDJSON event from a child into a bounded per-task progress record,
|
|
6
|
+
* and `finalizeTask` stamps the outcome from the runner's `StageResult`. The
|
|
7
|
+
* tool carries the folded records in its `details`, so every partial update the
|
|
8
|
+
* TUI sees is a complete picture of all tasks.
|
|
9
|
+
*
|
|
10
|
+
* The renderer half implements pi's per-tool rendering seam (`renderCall` /
|
|
11
|
+
* `renderResult`). Collapsed-while-running is the two-line pattern: a stable
|
|
12
|
+
* `agent — task` title over a churning `↳ current tool` line. Completion is a
|
|
13
|
+
* one-line receipt (`✓ agent · N tool uses · Xk tokens · Ys`); expanded shows
|
|
14
|
+
* the curated action log and the full report as markdown. String assembly is
|
|
15
|
+
* kept in pure helpers over a minimal {@link RenderTheme} so tests run against
|
|
16
|
+
* plain text — renderer exceptions are swallowed by pi (silently degrading to
|
|
17
|
+
* the bare title bar), so everything here must stay boringly total.
|
|
18
|
+
*/
|
|
19
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
21
|
+
import { narrationHeadline, toolLabel } from "./pipeline/activity.js";
|
|
22
|
+
import { SPINNER_FRAMES } from "./pipeline/activityFeed.js";
|
|
23
|
+
/** Bound on the retained action log so a chatty child cannot grow details unbounded. */
|
|
24
|
+
export const ACTION_LOG_MAX = 120;
|
|
25
|
+
/** How many of the newest actions the expanded view paints. */
|
|
26
|
+
const EXPANDED_ACTIONS_SHOWN = 30;
|
|
27
|
+
/** Collapsed report preview length, in lines. */
|
|
28
|
+
const REPORT_PREVIEW_LINES = 3;
|
|
29
|
+
/** Task preview width on the running title line. */
|
|
30
|
+
const TASK_PREVIEW_MAX = 64;
|
|
31
|
+
/** Spinner cadence; matches the /go feed's SPINNER_TICK_MS. */
|
|
32
|
+
const SPINNER_TICK_MS = 120;
|
|
33
|
+
const EMPTY_USAGE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
34
|
+
export function newTaskProgress(agent, task, startedAt) {
|
|
35
|
+
return {
|
|
36
|
+
agent,
|
|
37
|
+
task,
|
|
38
|
+
status: "running",
|
|
39
|
+
exitCode: -1,
|
|
40
|
+
startedAt,
|
|
41
|
+
toolCalls: 0,
|
|
42
|
+
toolErrors: 0,
|
|
43
|
+
usage: { ...EMPTY_USAGE },
|
|
44
|
+
actions: [],
|
|
45
|
+
droppedActions: 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function pushAction(p, entry) {
|
|
49
|
+
if (p.actions.length >= ACTION_LOG_MAX) {
|
|
50
|
+
p.actions.shift();
|
|
51
|
+
p.droppedActions += 1;
|
|
52
|
+
}
|
|
53
|
+
p.actions.push(entry);
|
|
54
|
+
}
|
|
55
|
+
/** Last text part of an assistant message, or undefined. */
|
|
56
|
+
function lastMessageText(ev) {
|
|
57
|
+
const parts = ev.message?.content ?? [];
|
|
58
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
59
|
+
const part = parts[i];
|
|
60
|
+
if (part?.type === "text" && typeof part.text === "string")
|
|
61
|
+
return part.text;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Fold one child NDJSON event into the task's progress. Returns true when the
|
|
67
|
+
* record changed (the tool emits an update), false for events we drop.
|
|
68
|
+
*/
|
|
69
|
+
export function applyChildEvent(p, ev) {
|
|
70
|
+
switch (ev.type) {
|
|
71
|
+
case "tool_execution_start": {
|
|
72
|
+
const entry = {
|
|
73
|
+
kind: "action",
|
|
74
|
+
text: toolLabel(ev.toolName ?? "", ev.args),
|
|
75
|
+
state: "running",
|
|
76
|
+
};
|
|
77
|
+
if (ev.toolCallId !== undefined)
|
|
78
|
+
entry.toolCallId = ev.toolCallId;
|
|
79
|
+
pushAction(p, entry);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
case "tool_execution_end": {
|
|
83
|
+
p.toolCalls += 1;
|
|
84
|
+
if (ev.isError)
|
|
85
|
+
p.toolErrors += 1;
|
|
86
|
+
for (let i = p.actions.length - 1; i >= 0; i--) {
|
|
87
|
+
const a = p.actions[i];
|
|
88
|
+
if (a.kind !== "action" || a.state !== "running")
|
|
89
|
+
continue;
|
|
90
|
+
if (ev.toolCallId !== undefined && a.toolCallId !== ev.toolCallId)
|
|
91
|
+
continue;
|
|
92
|
+
a.state = ev.isError ? "error" : "done";
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
case "message_end": {
|
|
98
|
+
const msg = ev.message;
|
|
99
|
+
if (msg?.role !== "assistant")
|
|
100
|
+
return false;
|
|
101
|
+
p.usage.turns += 1;
|
|
102
|
+
const u = msg.usage;
|
|
103
|
+
if (u) {
|
|
104
|
+
p.usage.input += u.input ?? 0;
|
|
105
|
+
p.usage.output += u.output ?? 0;
|
|
106
|
+
p.usage.cacheRead += u.cacheRead ?? 0;
|
|
107
|
+
p.usage.cacheWrite += u.cacheWrite ?? 0;
|
|
108
|
+
p.usage.cost += u.cost?.total ?? 0;
|
|
109
|
+
}
|
|
110
|
+
const text = lastMessageText(ev);
|
|
111
|
+
const headline = text !== undefined ? narrationHeadline(text) : null;
|
|
112
|
+
if (headline)
|
|
113
|
+
pushAction(p, { kind: "narration", text: headline, state: "done" });
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
default:
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Stamp the runner's outcome onto the progress record. */
|
|
121
|
+
export function finalizeTask(p, result, endedAt) {
|
|
122
|
+
// An aborted child can close with exit 0 (SIGTERM close reports a null code),
|
|
123
|
+
// so the stop reason outranks the exit code for the status glyph.
|
|
124
|
+
p.status = result.exitCode === 0 && result.stopReason !== "aborted" ? "done" : "error";
|
|
125
|
+
p.exitCode = result.exitCode;
|
|
126
|
+
p.endedAt = endedAt;
|
|
127
|
+
p.usage = { ...result.usage };
|
|
128
|
+
p.toolCalls = result.toolCalls;
|
|
129
|
+
p.toolErrors = result.toolErrors;
|
|
130
|
+
p.report = result.finalOutput;
|
|
131
|
+
if (result.stopReason)
|
|
132
|
+
p.stopReason = result.stopReason;
|
|
133
|
+
if (result.errorMessage)
|
|
134
|
+
p.errorMessage = result.errorMessage;
|
|
135
|
+
}
|
|
136
|
+
// ── Formatting helpers ────────────────────────────────────────────────────────
|
|
137
|
+
/** Collapse whitespace and clip to `max`, appending an ellipsis when cut. */
|
|
138
|
+
function clip(text, max) {
|
|
139
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
140
|
+
if (collapsed.length <= max)
|
|
141
|
+
return collapsed;
|
|
142
|
+
return `${collapsed.slice(0, max - 1)}…`;
|
|
143
|
+
}
|
|
144
|
+
function trimTrailingZero(v) {
|
|
145
|
+
const s = v.toFixed(1);
|
|
146
|
+
return s.endsWith(".0") ? s.slice(0, -2) : s;
|
|
147
|
+
}
|
|
148
|
+
/** 532 → "532", 41_234 → "41.2k", 1_240_000 → "1.2M". */
|
|
149
|
+
export function formatTokens(n) {
|
|
150
|
+
if (n < 1000)
|
|
151
|
+
return String(n);
|
|
152
|
+
if (n < 1_000_000)
|
|
153
|
+
return `${trimTrailingZero(n / 1000)}k`;
|
|
154
|
+
return `${trimTrailingZero(n / 1_000_000)}M`;
|
|
155
|
+
}
|
|
156
|
+
/** 42_000 → "42s", 81_000 → "1m 21s", 3_720_000 → "1h 2m". */
|
|
157
|
+
export function formatDuration(ms) {
|
|
158
|
+
const s = Math.max(0, Math.floor(ms / 1000));
|
|
159
|
+
if (s < 60)
|
|
160
|
+
return `${s}s`;
|
|
161
|
+
const m = Math.floor(s / 60);
|
|
162
|
+
if (m < 60)
|
|
163
|
+
return `${m}m ${s % 60}s`;
|
|
164
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
165
|
+
}
|
|
166
|
+
function countToolUses(n) {
|
|
167
|
+
return n === 1 ? "1 tool use" : `${n} tool uses`;
|
|
168
|
+
}
|
|
169
|
+
/** Tokens the child actually generated/consumed (cache reads excluded). */
|
|
170
|
+
function taskTokens(p) {
|
|
171
|
+
return p.usage.input + p.usage.output;
|
|
172
|
+
}
|
|
173
|
+
/** The most recent still-running tool action, if any. */
|
|
174
|
+
function currentActionText(p) {
|
|
175
|
+
for (let i = p.actions.length - 1; i >= 0; i--) {
|
|
176
|
+
const a = p.actions[i];
|
|
177
|
+
if (a.kind === "action" && a.state === "running")
|
|
178
|
+
return a.text;
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
/** What the child is doing right now: current tool → tool-use count → starting. */
|
|
183
|
+
function liveDetailText(p) {
|
|
184
|
+
return currentActionText(p) ?? (p.toolCalls > 0 ? countToolUses(p.toolCalls) : "starting…");
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The two-line live status for one running task: a stable `agent — task` title
|
|
188
|
+
* over the churning current-action line with elapsed time and live tokens.
|
|
189
|
+
*/
|
|
190
|
+
export function runningLines(p, theme, now, frame) {
|
|
191
|
+
const title = `${frame} ${theme.fg("accent", p.agent)} — ${theme.fg("dim", clip(p.task, TASK_PREVIEW_MAX))}`;
|
|
192
|
+
let detail = ` ${theme.fg("muted", "↳")} ${theme.fg("toolOutput", liveDetailText(p))}`;
|
|
193
|
+
detail += theme.fg("dim", ` · ${formatDuration(now - p.startedAt)}`);
|
|
194
|
+
const tokens = taskTokens(p);
|
|
195
|
+
if (tokens > 0)
|
|
196
|
+
detail += theme.fg("dim", ` · ${formatTokens(tokens)} tokens`);
|
|
197
|
+
return [title, detail];
|
|
198
|
+
}
|
|
199
|
+
/** One-line completion receipt: `✓ agent · N tool uses · Xk tokens · Ys · $c`. */
|
|
200
|
+
export function receiptLine(p, theme) {
|
|
201
|
+
const parts = [
|
|
202
|
+
countToolUses(p.toolCalls),
|
|
203
|
+
`${formatTokens(taskTokens(p))} tokens`,
|
|
204
|
+
formatDuration((p.endedAt ?? p.startedAt) - p.startedAt),
|
|
205
|
+
];
|
|
206
|
+
if (p.toolErrors > 0)
|
|
207
|
+
parts.push(`${p.toolErrors} tool ${p.toolErrors === 1 ? "error" : "errors"}`);
|
|
208
|
+
if (p.usage.cost > 0)
|
|
209
|
+
parts.push(`$${p.usage.cost.toFixed(2)}`);
|
|
210
|
+
const name = theme.bold(theme.fg("accent", p.agent));
|
|
211
|
+
if (p.status === "error") {
|
|
212
|
+
const reason = p.stopReason ?? (p.exitCode >= 0 ? `exit ${p.exitCode}` : "failed");
|
|
213
|
+
return `${theme.fg("error", "✗")} ${name} ${theme.fg("error", `failed (${reason})`)} ${theme.fg("dim", `· ${parts.join(" · ")}`)}`;
|
|
214
|
+
}
|
|
215
|
+
return `${theme.fg("success", "✓")} ${name} ${theme.fg("dim", `· ${parts.join(" · ")}`)}`;
|
|
216
|
+
}
|
|
217
|
+
/** Aggregate receipt across parallel tasks. */
|
|
218
|
+
function totalLine(tasks, theme) {
|
|
219
|
+
const toolCalls = tasks.reduce((n, p) => n + p.toolCalls, 0);
|
|
220
|
+
const tokens = tasks.reduce((n, p) => n + taskTokens(p), 0);
|
|
221
|
+
const cost = tasks.reduce((n, p) => n + p.usage.cost, 0);
|
|
222
|
+
const start = Math.min(...tasks.map((p) => p.startedAt));
|
|
223
|
+
const end = Math.max(...tasks.map((p) => p.endedAt ?? p.startedAt));
|
|
224
|
+
const parts = [countToolUses(toolCalls), `${formatTokens(tokens)} tokens`, formatDuration(end - start)];
|
|
225
|
+
if (cost > 0)
|
|
226
|
+
parts.push(`$${cost.toFixed(2)}`);
|
|
227
|
+
return theme.fg("dim", `${tasks.length} subagents · ${parts.join(" · ")}`);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Plain-text (no theme) summary for the partial result's `content`, so headless
|
|
231
|
+
* consumers and pi's fallback renderer still see live progress.
|
|
232
|
+
*/
|
|
233
|
+
export function progressSummaryText(tasks, now) {
|
|
234
|
+
const plain = { bold: (s) => s, fg: (_c, s) => s };
|
|
235
|
+
return tasks
|
|
236
|
+
.map((p) => p.status === "running"
|
|
237
|
+
? `${p.agent}: ${liveDetailText(p)} · ${formatDuration(now - p.startedAt)}`
|
|
238
|
+
: receiptLine(p, plain))
|
|
239
|
+
.join("\n");
|
|
240
|
+
}
|
|
241
|
+
/** The harness "Working…" replacement while subagents run. */
|
|
242
|
+
export function formatWorkingMessage(tasks, now) {
|
|
243
|
+
const toolCalls = tasks.reduce((n, p) => n + p.toolCalls, 0);
|
|
244
|
+
const started = Math.min(...tasks.map((p) => p.startedAt));
|
|
245
|
+
const elapsed = formatDuration(now - started);
|
|
246
|
+
if (tasks.length === 1) {
|
|
247
|
+
return `subagent ${tasks[0].agent} · ${countToolUses(toolCalls)} · ${elapsed}`;
|
|
248
|
+
}
|
|
249
|
+
const running = tasks.filter((p) => p.status === "running").length;
|
|
250
|
+
const label = running > 0 && running < tasks.length ? `${running}/${tasks.length} subagents` : `${tasks.length} subagents`;
|
|
251
|
+
return `${label} · ${countToolUses(toolCalls)} · ${elapsed}`;
|
|
252
|
+
}
|
|
253
|
+
/** Title painted the moment the call streams in (before any execution output). */
|
|
254
|
+
export function renderSubagentCall(args, theme, _context) {
|
|
255
|
+
const title = theme.fg("toolTitle", theme.bold("subagent"));
|
|
256
|
+
const a = args ?? {};
|
|
257
|
+
if (a.tasks && a.tasks.length > 0) {
|
|
258
|
+
let text = `${title} ${theme.fg("accent", `${a.tasks.length} parallel tasks`)}`;
|
|
259
|
+
for (const t of a.tasks) {
|
|
260
|
+
text += `\n ${theme.fg("accent", t.agent ?? "general")} ${theme.fg("dim", clip(t.task ?? "…", TASK_PREVIEW_MAX))}`;
|
|
261
|
+
}
|
|
262
|
+
return new Text(text, 0, 0);
|
|
263
|
+
}
|
|
264
|
+
let text = `${title} ${theme.fg("accent", a.agent ?? "general")}`;
|
|
265
|
+
if (a.task)
|
|
266
|
+
text += `\n ${theme.fg("dim", clip(a.task, 2 * TASK_PREVIEW_MAX))}`;
|
|
267
|
+
return new Text(text, 0, 0);
|
|
268
|
+
}
|
|
269
|
+
function fallbackText(result) {
|
|
270
|
+
const first = result.content.find((c) => c.type === "text" && typeof c.text === "string");
|
|
271
|
+
return new Text(first?.text ?? "(no output)", 0, 0);
|
|
272
|
+
}
|
|
273
|
+
function actionGlyph(a, theme) {
|
|
274
|
+
if (a.kind === "narration")
|
|
275
|
+
return theme.fg("muted", "·");
|
|
276
|
+
if (a.state === "error")
|
|
277
|
+
return theme.fg("error", "✗");
|
|
278
|
+
if (a.state === "running")
|
|
279
|
+
return theme.fg("muted", "→");
|
|
280
|
+
return theme.fg("muted", "→");
|
|
281
|
+
}
|
|
282
|
+
function actionLogLines(p, theme, shown) {
|
|
283
|
+
const lines = [];
|
|
284
|
+
const tail = p.actions.slice(-shown);
|
|
285
|
+
const skipped = p.droppedActions + (p.actions.length - tail.length);
|
|
286
|
+
if (skipped > 0)
|
|
287
|
+
lines.push(theme.fg("muted", ` … ${skipped} earlier actions`));
|
|
288
|
+
for (const a of tail) {
|
|
289
|
+
const color = a.kind === "narration" ? "dim" : a.state === "error" ? "error" : "toolOutput";
|
|
290
|
+
lines.push(` ${actionGlyph(a, theme)} ${theme.fg(color, clip(a.text, 110))}`);
|
|
291
|
+
}
|
|
292
|
+
return lines;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Fail-open degradation is still worth seeing: warn (not error — falling back to
|
|
296
|
+
* plain text is recoverable, nothing to page on) so an unexpected markdown
|
|
297
|
+
* failure is observable. Once per component, because the render path repaints on
|
|
298
|
+
* every frame and the "no TUI" case would otherwise flood the log.
|
|
299
|
+
*/
|
|
300
|
+
function warnMarkdownFallback(phase, err) {
|
|
301
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
302
|
+
try {
|
|
303
|
+
console.warn(JSON.stringify({
|
|
304
|
+
source: "yagni-subagent-render",
|
|
305
|
+
level: "warning",
|
|
306
|
+
message: "markdown rendering unavailable; falling back to plain text",
|
|
307
|
+
phase,
|
|
308
|
+
error: message,
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
console.warn(`[yagni-subagent-render] markdown ${phase} failed: ${message}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* A prose body as markdown when the TUI's markdown theme is available.
|
|
317
|
+
* `getMarkdownTheme()` hands back a lazy proxy that only throws when a style is
|
|
318
|
+
* first USED, so the fallback must wrap `render`, not construction — otherwise
|
|
319
|
+
* an uninitialized theme would blow up mid-paint and pi would silently degrade
|
|
320
|
+
* the whole row to the bare title bar.
|
|
321
|
+
*/
|
|
322
|
+
export function markdownOrPlain(body, theme) {
|
|
323
|
+
const plain = new Text(theme.fg("toolOutput", body.trim()), 0, 0);
|
|
324
|
+
let markdown;
|
|
325
|
+
try {
|
|
326
|
+
markdown = new Markdown(body.trim(), 0, 0, getMarkdownTheme());
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
warnMarkdownFallback("construct", err);
|
|
330
|
+
return plain;
|
|
331
|
+
}
|
|
332
|
+
let warned = false;
|
|
333
|
+
return {
|
|
334
|
+
render(width) {
|
|
335
|
+
try {
|
|
336
|
+
return markdown.render(width);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
if (!warned) {
|
|
340
|
+
warned = true;
|
|
341
|
+
warnMarkdownFallback("render", err);
|
|
342
|
+
}
|
|
343
|
+
return plain.render(width);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
invalidate() {
|
|
347
|
+
markdown.invalidate?.();
|
|
348
|
+
plain.invalidate();
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Result renderer: live two-line status per task while partial; receipts plus
|
|
354
|
+
* report preview when collapsed; action log plus full markdown report when
|
|
355
|
+
* expanded. Drives its own refresh while running via an unref'd interval on
|
|
356
|
+
* `context.state` (pi has no unmount hook — the final render clears it).
|
|
357
|
+
*/
|
|
358
|
+
export function renderSubagentResult(result, options, theme, context) {
|
|
359
|
+
const details = result.details;
|
|
360
|
+
const tasks = details?.tasks;
|
|
361
|
+
const state = context.state ?? {};
|
|
362
|
+
if (options.isPartial) {
|
|
363
|
+
if (!state.timer) {
|
|
364
|
+
state.timer = setInterval(() => context.invalidate(), SPINNER_TICK_MS);
|
|
365
|
+
state.timer.unref?.();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else if (state.timer) {
|
|
369
|
+
clearInterval(state.timer);
|
|
370
|
+
delete state.timer;
|
|
371
|
+
}
|
|
372
|
+
if (!tasks || tasks.length === 0)
|
|
373
|
+
return fallbackText(result);
|
|
374
|
+
if (options.isPartial) {
|
|
375
|
+
const now = Date.now();
|
|
376
|
+
const frame = SPINNER_FRAMES[Math.floor(now / SPINNER_TICK_MS) % SPINNER_FRAMES.length];
|
|
377
|
+
const lines = [];
|
|
378
|
+
for (const p of tasks) {
|
|
379
|
+
if (lines.length > 0)
|
|
380
|
+
lines.push("");
|
|
381
|
+
if (p.status === "running")
|
|
382
|
+
lines.push(...runningLines(p, theme, now, frame));
|
|
383
|
+
else
|
|
384
|
+
lines.push(receiptLine(p, theme));
|
|
385
|
+
if (options.expanded)
|
|
386
|
+
lines.push(...actionLogLines(p, theme, EXPANDED_ACTIONS_SHOWN));
|
|
387
|
+
}
|
|
388
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
389
|
+
}
|
|
390
|
+
if (options.expanded) {
|
|
391
|
+
const container = new Container();
|
|
392
|
+
tasks.forEach((p, i) => {
|
|
393
|
+
if (i > 0)
|
|
394
|
+
container.addChild(new Spacer(1));
|
|
395
|
+
container.addChild(new Text(receiptLine(p, theme), 0, 0));
|
|
396
|
+
container.addChild(new Text(theme.fg("dim", ` ${clip(p.task, 2 * TASK_PREVIEW_MAX)}`), 0, 0));
|
|
397
|
+
if (p.errorMessage)
|
|
398
|
+
container.addChild(new Text(theme.fg("error", ` ${clip(p.errorMessage, 200)}`), 0, 0));
|
|
399
|
+
const log = actionLogLines(p, theme, EXPANDED_ACTIONS_SHOWN);
|
|
400
|
+
if (log.length > 0)
|
|
401
|
+
container.addChild(new Text(log.join("\n"), 0, 0));
|
|
402
|
+
const report = (p.report ?? "").trim();
|
|
403
|
+
if (report) {
|
|
404
|
+
container.addChild(new Spacer(1));
|
|
405
|
+
container.addChild(markdownOrPlain(report, theme));
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
if (tasks.length > 1) {
|
|
409
|
+
container.addChild(new Spacer(1));
|
|
410
|
+
container.addChild(new Text(totalLine(tasks, theme), 0, 0));
|
|
411
|
+
}
|
|
412
|
+
return container;
|
|
413
|
+
}
|
|
414
|
+
// Collapsed, final: receipts + a short report preview per task.
|
|
415
|
+
const lines = [];
|
|
416
|
+
let truncated = false;
|
|
417
|
+
for (const p of tasks) {
|
|
418
|
+
if (lines.length > 0)
|
|
419
|
+
lines.push("");
|
|
420
|
+
lines.push(receiptLine(p, theme));
|
|
421
|
+
if (p.errorMessage)
|
|
422
|
+
lines.push(theme.fg("error", ` ${clip(p.errorMessage, 160)}`));
|
|
423
|
+
const report = (p.report ?? "").trim();
|
|
424
|
+
if (report) {
|
|
425
|
+
const reportLines = report.split("\n");
|
|
426
|
+
for (const l of reportLines.slice(0, REPORT_PREVIEW_LINES)) {
|
|
427
|
+
lines.push(theme.fg("toolOutput", ` ${l}`));
|
|
428
|
+
}
|
|
429
|
+
if (reportLines.length > REPORT_PREVIEW_LINES)
|
|
430
|
+
truncated = true;
|
|
431
|
+
}
|
|
432
|
+
if (p.actions.length > 0)
|
|
433
|
+
truncated = true;
|
|
434
|
+
}
|
|
435
|
+
if (tasks.length > 1)
|
|
436
|
+
lines.push("", totalLine(tasks, theme));
|
|
437
|
+
if (truncated)
|
|
438
|
+
lines.push(theme.fg("muted", " (ctrl+o to expand)"));
|
|
439
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
440
|
+
}
|
|
441
|
+
//# sourceMappingURL=subagentRender.js.map
|
|
@@ -20,10 +20,13 @@
|
|
|
20
20
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Type } from "typebox";
|
|
22
22
|
import { runStage } from "./pipeline/runner.js";
|
|
23
|
-
import type
|
|
23
|
+
import { type ModelTier, type PipelineStage } from "./pipeline/types.js";
|
|
24
|
+
import { renderSubagentCall, renderSubagentResult } from "./subagentRender.js";
|
|
24
25
|
export declare const SUBAGENT_TOOL_NAME = "subagent";
|
|
25
26
|
export declare const GENERAL_AGENT_NAME = "general";
|
|
26
27
|
export declare const MAX_PARALLEL_SUBAGENTS = 4;
|
|
28
|
+
/** Wider fan-out ceiling while the session is in ultra mode (/ultra). */
|
|
29
|
+
export declare const MAX_PARALLEL_SUBAGENTS_ULTRA = 8;
|
|
27
30
|
/**
|
|
28
31
|
* The default tool surface a subagent gets when its definition declares none:
|
|
29
32
|
* the full working set plus grounded answers, mirroring what a Claude Code
|
|
@@ -91,6 +94,8 @@ export interface MakeSubagentToolDeps {
|
|
|
91
94
|
runStageImpl?: typeof runStage;
|
|
92
95
|
discover?: (deps: DiscoverDeps) => SubagentDef[];
|
|
93
96
|
homeDir?: string;
|
|
97
|
+
/** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
|
|
98
|
+
isUltra?: () => boolean;
|
|
94
99
|
}
|
|
95
100
|
export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
96
101
|
name: string;
|
|
@@ -105,6 +110,8 @@ export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
|
105
110
|
agent: Type.TOptional<Type.TString>;
|
|
106
111
|
}>>>;
|
|
107
112
|
}>;
|
|
113
|
+
renderCall: typeof renderSubagentCall;
|
|
114
|
+
renderResult: typeof renderSubagentResult;
|
|
108
115
|
execute(_toolCallId: string, params: SubagentParams, signal?: AbortSignal, onUpdate?: (update: {
|
|
109
116
|
content: Array<{
|
|
110
117
|
type: "text";
|
|
@@ -125,19 +132,15 @@ export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
|
125
132
|
text: string;
|
|
126
133
|
}[];
|
|
127
134
|
details: {
|
|
128
|
-
tasks:
|
|
129
|
-
agent: string;
|
|
130
|
-
task: string;
|
|
131
|
-
exitCode: number;
|
|
132
|
-
usage: import("./pipeline/types.js").StageUsage;
|
|
133
|
-
toolCalls: number;
|
|
134
|
-
}[];
|
|
135
|
+
tasks: import("./subagentRender.js").SubagentTaskProgress[];
|
|
135
136
|
};
|
|
136
137
|
}>;
|
|
137
138
|
};
|
|
138
139
|
export interface RegisterSubagentsDeps {
|
|
139
140
|
discover?: (deps: DiscoverDeps) => SubagentDef[];
|
|
140
141
|
homeDir?: string;
|
|
142
|
+
/** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
|
|
143
|
+
isUltra?: () => boolean;
|
|
141
144
|
}
|
|
142
145
|
/** Wire the subagent tool and the /agents listing command. */
|
|
143
146
|
export declare function registerSubagents(pi: ExtensionAPI, deps?: RegisterSubagentsDeps): void;
|