@gavin-hu/my-pi-agent 0.16.0 → 0.17.0
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/README.md +14 -8
- package/README.zh-CN.md +14 -9
- package/extensions/bytefly/README.md +174 -101
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +10 -27
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +29 -1
- package/extensions/file-browse/client.ts +16 -463
- package/extensions/file-browse/html.ts +7 -5
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/router.ts +2 -2
- package/extensions/file-browse/styles.ts +8 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/index.ts +20 -45
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +88 -33
- package/extensions/plan/commands.ts +19 -4
- package/extensions/plan/config.ts +27 -0
- package/extensions/plan/plan-index.ts +3 -1
- package/extensions/plan/plans.ts +31 -13
- package/extensions/plan/review.ts +34 -14
- package/extensions/plan/runtime.ts +3 -1
- package/extensions/plan/steps.ts +43 -14
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +10 -10
- package/extensions/plan/tools/exit.ts +54 -43
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +22 -29
- package/extensions/plan/tools.ts +5 -2
- package/extensions/plan/tui.ts +9 -7
- package/extensions/plan/types.ts +8 -0
- package/extensions/plan/web-chrome.ts +101 -0
- package/extensions/plan/web-client.ts +98 -311
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +28 -2
- package/extensions/plan/web-router.ts +2 -2
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/subagent/README.md +25 -12
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/index.ts +10 -35
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +40 -18
- package/extensions/wechat/bridge.ts +38 -40
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +1 -1
- package/extensions/wechat/interaction.ts +25 -4
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +7 -5
- package/extensions/wechat/prompt.ts +19 -4
- package/extensions/wechat/qrcode.ts +9 -9
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -38
- package/lib/README.md +7 -6
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/interaction.ts +18 -1
- package/lib/report.ts +75 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +16 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +6 -2
- package/lib/tui/gauge.ts +9 -5
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +3 -1
- package/skills/changelog/SKILL.md +57 -45
- package/skills/next/SKILL.md +184 -0
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { isExtensionEnabled, resolveEffectiveCwd } from "../../lib/env.ts";
|
|
20
|
+
import { textResult } from "../../lib/tool-result.ts";
|
|
20
21
|
import {
|
|
21
22
|
type AgentConfig,
|
|
22
23
|
type AgentScope,
|
|
@@ -158,12 +159,10 @@ export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): voi
|
|
|
158
159
|
const resolution = resolveMode(args);
|
|
159
160
|
if ("error" in resolution) {
|
|
160
161
|
const roster = discoverAgents(root, args.agentScope ?? "user").agents;
|
|
161
|
-
return {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
details: { mode: "single", results: [] } satisfies SubagentDetails,
|
|
166
|
-
};
|
|
162
|
+
return textResult(`${resolution.error} Available agents: ${formatAgentList(roster)}`, {
|
|
163
|
+
mode: "single",
|
|
164
|
+
results: [],
|
|
165
|
+
} satisfies SubagentDetails);
|
|
167
166
|
}
|
|
168
167
|
|
|
169
168
|
const mode = resolution.mode;
|
|
@@ -195,17 +194,11 @@ export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): voi
|
|
|
195
194
|
.filter((agent): agent is AgentConfig => agent?.source === "project");
|
|
196
195
|
if (projectAgents.length > 0) {
|
|
197
196
|
if (!ctx.hasUI) {
|
|
198
|
-
return
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
"Refusing to run project-local agents without confirmation. " +
|
|
204
|
-
'Trust the project or use agentScope: "user".',
|
|
205
|
-
},
|
|
206
|
-
],
|
|
207
|
-
details: makeDetails([]),
|
|
208
|
-
};
|
|
197
|
+
return textResult(
|
|
198
|
+
"Refusing to run project-local agents without confirmation. " +
|
|
199
|
+
'Trust the project or use agentScope: "user".',
|
|
200
|
+
makeDetails([]),
|
|
201
|
+
);
|
|
209
202
|
}
|
|
210
203
|
const names = projectAgents.map((agent) => agent.name).join(", ");
|
|
211
204
|
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
|
@@ -214,31 +207,20 @@ export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): voi
|
|
|
214
207
|
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
215
208
|
);
|
|
216
209
|
if (!approved) {
|
|
217
|
-
return
|
|
218
|
-
content: [{ type: "text" as const, text: "Canceled: project-local agents not approved." }],
|
|
219
|
-
details: makeDetails([]),
|
|
220
|
-
};
|
|
210
|
+
return textResult("Canceled: project-local agents not approved.", makeDetails([]));
|
|
221
211
|
}
|
|
222
212
|
}
|
|
223
213
|
}
|
|
224
214
|
|
|
225
215
|
if (args.background) {
|
|
226
216
|
if (mode !== "single") {
|
|
227
|
-
return
|
|
228
|
-
content: [{ type: "text" as const, text: "`background` supports single mode only." }],
|
|
229
|
-
details: makeDetails([]),
|
|
230
|
-
};
|
|
217
|
+
return textResult("`background` supports single mode only.", makeDetails([]));
|
|
231
218
|
}
|
|
232
219
|
if (!agents.some((candidate) => candidate.name === args.agent)) {
|
|
233
|
-
return
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
text: `Unknown agent: "${args.agent ?? ""}". Available agents: ${formatAgentList(agents)}`,
|
|
238
|
-
},
|
|
239
|
-
],
|
|
240
|
-
details: makeDetails([]),
|
|
241
|
-
};
|
|
220
|
+
return textResult(
|
|
221
|
+
`Unknown agent: "${args.agent ?? ""}". Available agents: ${formatAgentList(agents)}`,
|
|
222
|
+
makeDetails([]),
|
|
223
|
+
);
|
|
242
224
|
}
|
|
243
225
|
const started = background.start(
|
|
244
226
|
{
|
|
@@ -259,19 +241,11 @@ export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): voi
|
|
|
259
241
|
},
|
|
260
242
|
);
|
|
261
243
|
if (!started.ok) {
|
|
262
|
-
return
|
|
263
|
-
content: [{ type: "text" as const, text: started.error }],
|
|
264
|
-
details: makeDetails([]),
|
|
265
|
-
};
|
|
244
|
+
return textResult(started.error, makeDetails([]));
|
|
266
245
|
}
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
type: "text" as const,
|
|
271
|
-
text: `Started ${started.run.agent} as ${started.run.id} in the background. Use /agents to inspect, collect, or cancel it.`,
|
|
272
|
-
},
|
|
273
|
-
],
|
|
274
|
-
details: {
|
|
246
|
+
return textResult(
|
|
247
|
+
`Started ${started.run.agent} as ${started.run.id} in the background. Use /agents to inspect, collect, or cancel it.`,
|
|
248
|
+
{
|
|
275
249
|
...makeDetails([]),
|
|
276
250
|
background: {
|
|
277
251
|
runId: started.run.id,
|
|
@@ -281,7 +255,7 @@ export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): voi
|
|
|
281
255
|
startedAt: started.run.startedAt,
|
|
282
256
|
},
|
|
283
257
|
} satisfies SubagentDetails,
|
|
284
|
-
|
|
258
|
+
);
|
|
285
259
|
}
|
|
286
260
|
|
|
287
261
|
const context: ModeContext = {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
8
|
+
import { failedResult, textResult } from "../../lib/tool-result.ts";
|
|
8
9
|
import type { AgentConfig } from "./agents.ts";
|
|
9
10
|
import type { TranscriptMode } from "./config.ts";
|
|
10
11
|
import type { RunOptions } from "./run.ts";
|
|
@@ -73,25 +74,16 @@ export async function runChainMode(ctx: ModeContext): Promise<AgentToolResult<Su
|
|
|
73
74
|
results.push(result);
|
|
74
75
|
|
|
75
76
|
if (isFailedResult(result)) {
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
text: `Chain stopped at step ${i + 1} (${step.agent}): ${getResultOutput(result)}`,
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
details: makeDetails(results),
|
|
84
|
-
isError: true,
|
|
85
|
-
};
|
|
77
|
+
return failedResult(
|
|
78
|
+
`Chain stopped at step ${i + 1} (${step.agent}): ${getResultOutput(result)}`,
|
|
79
|
+
makeDetails(results),
|
|
80
|
+
);
|
|
86
81
|
}
|
|
87
82
|
previous = getFinalOutput(result.messages);
|
|
88
83
|
}
|
|
89
84
|
|
|
90
85
|
const last = results[results.length - 1];
|
|
91
|
-
return
|
|
92
|
-
content: [{ type: "text" as const, text: last ? getFinalOutput(last.messages) || "(no output)" : "(no output)" }],
|
|
93
|
-
details: makeDetails(results),
|
|
94
|
-
};
|
|
86
|
+
return textResult(last ? getFinalOutput(last.messages) || "(no output)" : "(no output)", makeDetails(results));
|
|
95
87
|
}
|
|
96
88
|
|
|
97
89
|
/** Independent tasks with bounded concurrency, reporting running/done progress. */
|
|
@@ -104,10 +96,9 @@ export async function runParallelMode(ctx: ModeContext): Promise<AgentToolResult
|
|
|
104
96
|
if (!onUpdate) return;
|
|
105
97
|
const running = allResults.filter((r) => r.exitCode === -1).length;
|
|
106
98
|
const done = allResults.length - running;
|
|
107
|
-
onUpdate(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
99
|
+
onUpdate(
|
|
100
|
+
textResult(`Parallel: ${done}/${allResults.length} done, ${running} running…`, makeDetails([...allResults])),
|
|
101
|
+
);
|
|
111
102
|
};
|
|
112
103
|
|
|
113
104
|
const results = await mapWithConcurrencyLimit(tasks, MAX_CONCURRENCY, async (task, index) => {
|
|
@@ -146,15 +137,10 @@ export async function runParallelMode(ctx: ModeContext): Promise<AgentToolResult
|
|
|
146
137
|
return `### [${result.agent}] ${status}\n\n${output}`;
|
|
147
138
|
});
|
|
148
139
|
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n---\n\n")}`,
|
|
154
|
-
},
|
|
155
|
-
],
|
|
156
|
-
details: makeDetails(results),
|
|
157
|
-
};
|
|
140
|
+
return textResult(
|
|
141
|
+
`Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n---\n\n")}`,
|
|
142
|
+
makeDetails(results),
|
|
143
|
+
);
|
|
158
144
|
}
|
|
159
145
|
|
|
160
146
|
/** One agent + task. */
|
|
@@ -176,20 +162,11 @@ export async function runSingleMode(ctx: ModeContext): Promise<AgentToolResult<S
|
|
|
176
162
|
});
|
|
177
163
|
|
|
178
164
|
if (isFailedResult(result)) {
|
|
179
|
-
return
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
text: `Agent ${result.agent} ${result.stopReason ?? "failed"}: ${getResultOutput(result)}`,
|
|
184
|
-
},
|
|
185
|
-
],
|
|
186
|
-
details: makeDetails([result]),
|
|
187
|
-
isError: true,
|
|
188
|
-
};
|
|
165
|
+
return failedResult(
|
|
166
|
+
`Agent ${result.agent} ${result.stopReason ?? "failed"}: ${getResultOutput(result)}`,
|
|
167
|
+
makeDetails([result]),
|
|
168
|
+
);
|
|
189
169
|
}
|
|
190
170
|
|
|
191
|
-
return
|
|
192
|
-
content: [{ type: "text" as const, text: getFinalOutput(result.messages) || "(no output)" }],
|
|
193
|
-
details: makeDetails([result]),
|
|
194
|
-
};
|
|
171
|
+
return textResult(getFinalOutput(result.messages) || "(no output)", makeDetails([result]));
|
|
195
172
|
}
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Status and error rendering is shared across single- and multi-result modes so
|
|
10
10
|
* a failed task never loses its message, regardless of how many tasks ran.
|
|
11
|
+
*
|
|
12
|
+
* The usage footnote follows the same grammar in every mode: a blank line, the
|
|
13
|
+
* faint metrics row, then the expand hint, so the affordance is always the
|
|
14
|
+
* block's last line. `format.ts` owns the row's text; it carries no money,
|
|
15
|
+
* because a price needs the footer's currency and tariff.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
@@ -20,7 +25,7 @@ import { expandHint, GLYPHS, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
|
20
25
|
import { centeredRule } from "../../lib/tui/rules.ts";
|
|
21
26
|
import { BODY_INDENT } from "../../lib/tui/indent.ts";
|
|
22
27
|
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
23
|
-
import {
|
|
28
|
+
import { clip, formatToolCall, totalUsageLine, usageLine } from "./format.ts";
|
|
24
29
|
import { COLLAPSED_ERROR_MAX, COLLAPSED_ITEM_COUNT, COLLAPSED_TEXT_LINES, type SubagentArgs } from "./schema.ts";
|
|
25
30
|
import { getFinalOutput, isFailedResult } from "./stream.ts";
|
|
26
31
|
import type { SingleResult, SubagentDetails } from "./types.ts";
|
|
@@ -132,27 +137,6 @@ function isExpandable(result: SingleResult, status: ResultStatus, items: Display
|
|
|
132
137
|
return hasExpandableContent(items) || hasClippedError(result, status);
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
/** "2 tool errors" / "1 tool error". */
|
|
136
|
-
function toolErrorsLabel(count: number): string {
|
|
137
|
-
return `${count} tool error${count > 1 ? "s" : ""}`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Usage plus a tool-error count when the subagent hit any. */
|
|
141
|
-
function usageLine(result: SingleResult): string {
|
|
142
|
-
const base = formatUsageStats(result.usage, result.model);
|
|
143
|
-
if (!result.toolErrors) return base;
|
|
144
|
-
const label = toolErrorsLabel(result.toolErrors);
|
|
145
|
-
return base ? `${base} ${label}` : label;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** The `Total:` usage line for a multi run, including summed tool errors. */
|
|
149
|
-
function totalUsageLine(results: SingleResult[]): string {
|
|
150
|
-
const usage = formatUsageStats(aggregateUsage(results));
|
|
151
|
-
const errors = results.reduce((sum, result) => sum + (result.toolErrors ?? 0), 0);
|
|
152
|
-
const label = errors ? toolErrorsLabel(errors) : "";
|
|
153
|
-
return [usage, label].filter(Boolean).join(" ");
|
|
154
|
-
}
|
|
155
|
-
|
|
156
140
|
/** " 12s" while running or once finished, or "" when no start time was recorded. */
|
|
157
141
|
function durationSuffix(result: SingleResult, status: ResultStatus, now: number): string {
|
|
158
142
|
if (!result.startedAt) return "";
|
|
@@ -306,6 +290,22 @@ function transcriptLine(result: SingleResult, theme: Theme): string | undefined
|
|
|
306
290
|
return paint(theme, "faint", `transcript: ${sanitizeLine(result.artifactPath)}${cut}`);
|
|
307
291
|
}
|
|
308
292
|
|
|
293
|
+
/** A blank line and one faint row: the shape every expanded footnote takes. */
|
|
294
|
+
function addFaintRow(container: Container, theme: Theme, text: string): void {
|
|
295
|
+
container.addChild(new Spacer(1));
|
|
296
|
+
container.addChild(new Text(paint(theme, "faint", text), 0, 0));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The collapsed block's footnote: a blank line, the faint metrics row, then the
|
|
301
|
+
* expand hint. Both collapsed views end through here, so the metrics row and the
|
|
302
|
+
* affordance cannot swap places again.
|
|
303
|
+
*/
|
|
304
|
+
function collapsedFooter(theme: Theme, metrics: string, hinted: boolean): string {
|
|
305
|
+
const rows = metrics ? `\n\n${paint(theme, "faint", metrics)}` : "";
|
|
306
|
+
return hinted ? `${rows}\n${expandHint(theme)}` : rows;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
309
|
/** Append one result's expanded block: header, error, task, output, usage. */
|
|
310
310
|
function addExpandedResult(
|
|
311
311
|
container: Container,
|
|
@@ -359,15 +359,12 @@ function addExpandedResult(
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
const usage = usageLine(result);
|
|
362
|
-
if (usage)
|
|
363
|
-
container.addChild(new Spacer(1));
|
|
364
|
-
container.addChild(new Text(paint(theme, "faint", usage), 0, 0));
|
|
365
|
-
}
|
|
362
|
+
if (usage) addFaintRow(container, theme, usage);
|
|
366
363
|
const transcript = transcriptLine(result, theme);
|
|
367
364
|
if (transcript) container.addChild(new Text(transcript, 0, 0));
|
|
368
365
|
}
|
|
369
366
|
|
|
370
|
-
/** The collapsed single result: header, a word, body,
|
|
367
|
+
/** The collapsed single result: header, a word, body, and the shared footer. */
|
|
371
368
|
function collapsedSingleText(result: SingleResult, theme: Theme, isPartial: boolean, now: number): string {
|
|
372
369
|
const status = resultStatus(result, isPartial);
|
|
373
370
|
// The leading blank line separates the result block from the call line, matching
|
|
@@ -377,10 +374,7 @@ function collapsedSingleText(result: SingleResult, theme: Theme, isPartial: bool
|
|
|
377
374
|
const displayItems = getDisplayItems(result.messages);
|
|
378
375
|
// A blank line separates the header from the body, matching the multi view.
|
|
379
376
|
text += `\n\n${collapsedBody(result, status, displayItems, theme)}`;
|
|
380
|
-
|
|
381
|
-
const usage = usageLine(result);
|
|
382
|
-
if (usage) text += `\n${paint(theme, "faint", usage)}`;
|
|
383
|
-
return text;
|
|
377
|
+
return text + collapsedFooter(theme, usageLine(result), isExpandable(result, status, displayItems));
|
|
384
378
|
}
|
|
385
379
|
|
|
386
380
|
function summarizeResults(results: SingleResult[]) {
|
|
@@ -445,12 +439,8 @@ function collapsedMultiText(details: SubagentDetails, theme: Theme, now: number)
|
|
|
445
439
|
text += statusSuffix(result, status, theme, now);
|
|
446
440
|
text += `\n${collapsedBody(result, status, displayItems, theme)}`;
|
|
447
441
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (usage) text += `\n\n${paint(theme, "faint", `Total: ${usage}`)}`;
|
|
451
|
-
}
|
|
452
|
-
if (running > 0 || hasHidden) text += `\n${expandHint(theme)}`;
|
|
453
|
-
return text;
|
|
442
|
+
const metrics = running === 0 ? totalUsageLine(results) : "";
|
|
443
|
+
return text + collapsedFooter(theme, metrics, running > 0 || hasHidden);
|
|
454
444
|
}
|
|
455
445
|
|
|
456
446
|
function fillExpandedMulti(container: Container, details: SubagentDetails, theme: Theme, now: number): void {
|
|
@@ -464,11 +454,8 @@ function fillExpandedMulti(container: Container, details: SubagentDetails, theme
|
|
|
464
454
|
|
|
465
455
|
const { running } = summarizeResults(details.results);
|
|
466
456
|
if (running === 0) {
|
|
467
|
-
const
|
|
468
|
-
if (
|
|
469
|
-
container.addChild(new Spacer(1));
|
|
470
|
-
container.addChild(new Text(paint(theme, "faint", `Total: ${usage}`), 0, 0));
|
|
471
|
-
}
|
|
457
|
+
const total = totalUsageLine(details.results);
|
|
458
|
+
if (total) addFaintRow(container, theme, total);
|
|
472
459
|
}
|
|
473
460
|
}
|
|
474
461
|
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Message } from "@earendil-works/pi-ai";
|
|
9
|
+
import { asNumber, asRecord } from "../../lib/config.ts";
|
|
10
|
+
import { USAGE_COUNTERS, type UsageCounts, zeroUsage } from "../../lib/usage.ts";
|
|
9
11
|
import type { SingleResult, UsageStats } from "./types.ts";
|
|
10
12
|
|
|
11
13
|
/** A zeroed usage accumulator. */
|
|
12
14
|
export function emptyUsage(): UsageStats {
|
|
13
|
-
return {
|
|
15
|
+
return { ...zeroUsage(), contextTokens: 0, turns: 0 };
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/** A fresh result for one subagent run; `exitCode: -1` marks it still running. */
|
|
@@ -29,6 +31,23 @@ export function parseJsonLine(line: string): Record<string, unknown> | undefined
|
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The counters and context size one assistant message reported.
|
|
36
|
+
*
|
|
37
|
+
* The payload is a child process's JSON, so every number is read defensively: a
|
|
38
|
+
* string, `NaN`, or a missing field contributes nothing instead of concatenating
|
|
39
|
+
* into the accumulator or publishing a bogus recorded cost. `contextTokens` is
|
|
40
|
+
* the message's `totalTokens`, the occupancy of the child's window at that turn.
|
|
41
|
+
*/
|
|
42
|
+
function readUsage(raw: unknown): { counters: UsageCounts; contextTokens?: number } {
|
|
43
|
+
const usage = asRecord(raw);
|
|
44
|
+
const counters = zeroUsage();
|
|
45
|
+
for (const counter of USAGE_COUNTERS) counters[counter] = asNumber(usage?.[counter]) ?? 0;
|
|
46
|
+
// The wire nests the dollar amount one level down, as `{ total }`.
|
|
47
|
+
counters.cost = asNumber(asRecord(usage?.cost)?.total) ?? 0;
|
|
48
|
+
return { counters, contextTokens: asNumber(usage?.totalTokens) };
|
|
49
|
+
}
|
|
50
|
+
|
|
32
51
|
/**
|
|
33
52
|
* Fold one parsed event into `result`.
|
|
34
53
|
*
|
|
@@ -51,25 +70,12 @@ export function applyEvent(result: SingleResult, event: unknown): boolean {
|
|
|
51
70
|
model?: string;
|
|
52
71
|
stopReason?: string;
|
|
53
72
|
errorMessage?: string;
|
|
54
|
-
usage?:
|
|
55
|
-
input?: number;
|
|
56
|
-
output?: number;
|
|
57
|
-
cacheRead?: number;
|
|
58
|
-
cacheWrite?: number;
|
|
59
|
-
totalTokens?: number;
|
|
60
|
-
cost?: { total?: number };
|
|
61
|
-
};
|
|
73
|
+
usage?: unknown;
|
|
62
74
|
};
|
|
63
75
|
result.usage.turns++;
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
result.usage.output += usage.output ?? 0;
|
|
68
|
-
result.usage.cacheRead += usage.cacheRead ?? 0;
|
|
69
|
-
result.usage.cacheWrite += usage.cacheWrite ?? 0;
|
|
70
|
-
result.usage.cost += usage.cost?.total ?? 0;
|
|
71
|
-
result.usage.contextTokens = usage.totalTokens ?? result.usage.contextTokens;
|
|
72
|
-
}
|
|
76
|
+
const reported = readUsage(assistant.usage);
|
|
77
|
+
for (const counter of USAGE_COUNTERS) result.usage[counter] += reported.counters[counter];
|
|
78
|
+
if (reported.contextTokens !== undefined) result.usage.contextTokens = reported.contextTokens;
|
|
73
79
|
if (!result.provider && assistant.provider) result.provider = assistant.provider;
|
|
74
80
|
if (!result.model && assistant.model) result.model = assistant.model;
|
|
75
81
|
if (assistant.stopReason) result.stopReason = assistant.stopReason;
|
|
@@ -24,6 +24,13 @@ export interface UsageStats {
|
|
|
24
24
|
turns: number;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** The fields a usage line reads from one result. */
|
|
28
|
+
export interface UsageSource {
|
|
29
|
+
usage: UsageStats;
|
|
30
|
+
model?: string;
|
|
31
|
+
toolErrors?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
/** Everything captured from one subagent subprocess. */
|
|
28
35
|
export interface SingleResult {
|
|
29
36
|
agent: string;
|
package/extensions/todo/tools.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { TODO_TOOL } from "../../lib/tool-names.ts";
|
|
16
|
+
import { failedResult, textResult } from "../../lib/tool-result.ts";
|
|
16
17
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
17
18
|
import { normalizeTodos } from "../../lib/todo-state.ts";
|
|
18
19
|
import { callLine, detailsOrLine, rejectionLine } from "../../lib/tui/tool.ts";
|
|
@@ -54,12 +55,10 @@ function toStructuredContent(todos: Todo[], action: "write" | "clear", error?: s
|
|
|
54
55
|
*/
|
|
55
56
|
function todoResult(todos: Todo[], action: "write" | "clear", error?: string) {
|
|
56
57
|
const details: TodoDetails = error === undefined ? { todos, action } : { todos, action, error };
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
details,
|
|
60
|
-
|
|
61
|
-
...(error === undefined ? {} : { isError: true as const }),
|
|
62
|
-
};
|
|
58
|
+
const structuredContent = toStructuredContent(todos, action, error);
|
|
59
|
+
return error === undefined
|
|
60
|
+
? textResult(formatTodoText(todos), details, structuredContent)
|
|
61
|
+
: failedResult(`Error: ${error}`, details, structuredContent);
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
export function registerTools(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
package/extensions/todo/tui.ts
CHANGED
|
@@ -161,13 +161,15 @@ export class TodoListComponent implements Component {
|
|
|
161
161
|
|
|
162
162
|
if (this.todos.length === 0) {
|
|
163
163
|
return dockListFrame({
|
|
164
|
+
theme: this.theme,
|
|
165
|
+
width: w,
|
|
164
166
|
header,
|
|
165
167
|
body: emptyState(this.theme, w, { label: "No todos yet. Ask the agent to plan some work." }),
|
|
166
168
|
hint: screenHint(this.theme, w, ["Esc close"]),
|
|
167
169
|
});
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
// header, summary, blank, blank-after-list, hint,
|
|
172
|
+
// header, summary, blank, blank-after-list, hint, closing rule
|
|
171
173
|
const chrome = 6;
|
|
172
174
|
this.visible = fitRows(this.viewportRowsSource, this.todos.length, chrome, SCREEN_DEFAULT_ITEMS);
|
|
173
175
|
this.scrollTop = clampScroll(this.scrollTop, this.todos.length, this.visible);
|
|
@@ -177,6 +179,8 @@ export class TodoListComponent implements Component {
|
|
|
177
179
|
for (let i = this.scrollTop; i < end; i++) body.push(todoRow(this.todos[i], this.theme, w));
|
|
178
180
|
|
|
179
181
|
return dockListFrame({
|
|
182
|
+
theme: this.theme,
|
|
183
|
+
width: w,
|
|
180
184
|
header,
|
|
181
185
|
summary: clip(` ${paint(this.theme, "meta", progressSummary(this.todos))}`, w),
|
|
182
186
|
body,
|
|
@@ -15,7 +15,7 @@ import { isExtensionEnabled } from "../../lib/env.ts";
|
|
|
15
15
|
import { WATCH_MATCH_CHANNEL, type WatchMatchEvent } from "../../lib/events.ts";
|
|
16
16
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
17
17
|
import { noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
|
|
18
|
-
import { createPendingReporter } from "../../lib/report.ts";
|
|
18
|
+
import { createPendingReporter, registerPendingChannel } from "../../lib/report.ts";
|
|
19
19
|
import { registerCommands } from "./commands.ts";
|
|
20
20
|
import { clipLabel } from "../../lib/tui/columns.ts";
|
|
21
21
|
import { formatPendingMatch } from "./format.ts";
|
|
@@ -97,41 +97,16 @@ export default function watch(pi: ExtensionAPI, deps: WatchDeps = {}): void {
|
|
|
97
97
|
registerTools(pi, runtime);
|
|
98
98
|
registerCommands(pi, runtime);
|
|
99
99
|
|
|
100
|
-
pi
|
|
101
|
-
reporter
|
|
102
|
-
runtime
|
|
100
|
+
registerPendingChannel(pi, {
|
|
101
|
+
reporter,
|
|
102
|
+
runtime,
|
|
103
103
|
// `shutdown` clears this; a reload or session replacement reuses the
|
|
104
104
|
// runtime, so re-arm it on every session start.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
pi.on("agent_start", () => {
|
|
114
|
-
reporter.agentStart();
|
|
115
|
-
});
|
|
116
|
-
pi.on("agent_settled", () => {
|
|
117
|
-
reporter.agentSettled();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Report matches to the model at the start of the next turn.
|
|
121
|
-
pi.on("before_agent_start", (_event, ctx) => {
|
|
122
|
-
const reported = reporter.beforeAgentStart();
|
|
123
|
-
if (!reported) return undefined;
|
|
124
|
-
runtime.setStatus(ctx);
|
|
125
|
-
return reported;
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Keep stale match notices out of later turns. Only this extension's notices
|
|
129
|
-
// are pruned; another source's are left for its own handler.
|
|
130
|
-
pi.on("context", (event) => reporter.filterContext(event.messages));
|
|
131
|
-
|
|
132
|
-
// `shutdown` detaches the UI controller, which clears both chips.
|
|
133
|
-
pi.on("session_shutdown", async () => {
|
|
134
|
-
runtime.onMatch = undefined;
|
|
135
|
-
await runtime.shutdown();
|
|
105
|
+
onSessionStart: () => {
|
|
106
|
+
runtime.onMatch = () => reporter.report();
|
|
107
|
+
},
|
|
108
|
+
onSessionShutdown: () => {
|
|
109
|
+
runtime.onMatch = undefined;
|
|
110
|
+
},
|
|
136
111
|
});
|
|
137
112
|
}
|