@bdsqqq/pi 0.0.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.
Files changed (99) hide show
  1. package/dist/chunk-DsIazq42.js +26 -0
  2. package/dist/command-palette-B97yKqne.js +406 -0
  3. package/dist/core/agents-md.d.ts +22 -0
  4. package/dist/core/agents-md.js +174 -0
  5. package/dist/core/box-chrome.d.ts +56 -0
  6. package/dist/core/box-chrome.js +207 -0
  7. package/dist/core/box-format.d.ts +82 -0
  8. package/dist/core/box-format.js +381 -0
  9. package/dist/core/config.d.ts +14 -0
  10. package/dist/core/config.js +201 -0
  11. package/dist/core/file-tracker.d.ts +65 -0
  12. package/dist/core/file-tracker.js +439 -0
  13. package/dist/core/github-api.d.ts +45 -0
  14. package/dist/core/github-api.js +101 -0
  15. package/dist/core/html-to-md.d.ts +5 -0
  16. package/dist/core/html-to-md.js +289 -0
  17. package/dist/core/interpolate.d.ts +44 -0
  18. package/dist/core/interpolate.js +475 -0
  19. package/dist/core/mutex.d.ts +8 -0
  20. package/dist/core/mutex.js +142 -0
  21. package/dist/core/output-buffer.d.ts +80 -0
  22. package/dist/core/output-buffer.js +517 -0
  23. package/dist/core/permissions.d.ts +19 -0
  24. package/dist/core/permissions.js +140 -0
  25. package/dist/core/pi-spawn.d.ts +62 -0
  26. package/dist/core/pi-spawn.js +244 -0
  27. package/dist/core/prompt-patch.d.ts +11 -0
  28. package/dist/core/prompt-patch.js +16 -0
  29. package/dist/core/show-renderer.d.ts +10 -0
  30. package/dist/core/show-renderer.js +39 -0
  31. package/dist/core/show.d.ts +29 -0
  32. package/dist/core/show.js +288 -0
  33. package/dist/core/sub-agent-render.d.ts +56 -0
  34. package/dist/core/sub-agent-render.js +579 -0
  35. package/dist/core/tool-cost.d.ts +17 -0
  36. package/dist/core/tool-cost.js +58 -0
  37. package/dist/core/tui.d.ts +9 -0
  38. package/dist/core/tui.js +33 -0
  39. package/dist/editor-BljciPdx.js +537 -0
  40. package/dist/extensions/bash.d.ts +12 -0
  41. package/dist/extensions/bash.js +261 -0
  42. package/dist/extensions/code-review.d.ts +14 -0
  43. package/dist/extensions/code-review.js +175 -0
  44. package/dist/extensions/command-palette.d.ts +6 -0
  45. package/dist/extensions/command-palette.js +2 -0
  46. package/dist/extensions/create-file.d.ts +7 -0
  47. package/dist/extensions/create-file.js +88 -0
  48. package/dist/extensions/edit-file.d.ts +7 -0
  49. package/dist/extensions/edit-file.js +395 -0
  50. package/dist/extensions/editor.d.ts +6 -0
  51. package/dist/extensions/editor.js +2 -0
  52. package/dist/extensions/finder.d.ts +13 -0
  53. package/dist/extensions/finder.js +108 -0
  54. package/dist/extensions/format-file.d.ts +11 -0
  55. package/dist/extensions/format-file.js +145 -0
  56. package/dist/extensions/github.d.ts +13 -0
  57. package/dist/extensions/github.js +527 -0
  58. package/dist/extensions/glob.d.ts +10 -0
  59. package/dist/extensions/glob.js +163 -0
  60. package/dist/extensions/grep.d.ts +13 -0
  61. package/dist/extensions/grep.js +356 -0
  62. package/dist/extensions/handoff.d.ts +6 -0
  63. package/dist/extensions/handoff.js +277 -0
  64. package/dist/extensions/librarian.d.ts +13 -0
  65. package/dist/extensions/librarian.js +113 -0
  66. package/dist/extensions/look-at.d.ts +13 -0
  67. package/dist/extensions/look-at.js +121 -0
  68. package/dist/extensions/ls.d.ts +8 -0
  69. package/dist/extensions/ls.js +87 -0
  70. package/dist/extensions/mermaid.d.ts +20 -0
  71. package/dist/extensions/mermaid.js +2 -0
  72. package/dist/extensions/oracle.d.ts +13 -0
  73. package/dist/extensions/oracle.js +143 -0
  74. package/dist/extensions/read-session.d.ts +13 -0
  75. package/dist/extensions/read-session.js +265 -0
  76. package/dist/extensions/read-web-page.d.ts +11 -0
  77. package/dist/extensions/read-web-page.js +234 -0
  78. package/dist/extensions/read.d.ts +23 -0
  79. package/dist/extensions/read.js +323 -0
  80. package/dist/extensions/search-sessions.d.ts +29 -0
  81. package/dist/extensions/search-sessions.js +426 -0
  82. package/dist/extensions/session-name.d.ts +6 -0
  83. package/dist/extensions/session-name.js +54 -0
  84. package/dist/extensions/skill.d.ts +7 -0
  85. package/dist/extensions/skill.js +232 -0
  86. package/dist/extensions/system-prompt.d.ts +6 -0
  87. package/dist/extensions/system-prompt.js +31 -0
  88. package/dist/extensions/task.d.ts +11 -0
  89. package/dist/extensions/task.js +114 -0
  90. package/dist/extensions/tool-harness.d.ts +6 -0
  91. package/dist/extensions/tool-harness.js +37 -0
  92. package/dist/extensions/undo-edit.d.ts +7 -0
  93. package/dist/extensions/undo-edit.js +127 -0
  94. package/dist/extensions/web-search.d.ts +12 -0
  95. package/dist/extensions/web-search.js +254 -0
  96. package/dist/extensions.d.ts +29 -0
  97. package/dist/extensions.js +29 -0
  98. package/dist/mermaid-C7xCVBrw.js +95296 -0
  99. package/package.json +67 -0
@@ -0,0 +1,56 @@
1
+ import { UsageStats } from "./pi-spawn.js";
2
+ import { ToolCostDetails } from "./tool-cost.js";
3
+ import { Container } from "@mariozechner/pi-tui";
4
+ import { Message } from "@mariozechner/pi-ai";
5
+
6
+ //#region packages/core/sub-agent-render/index.d.ts
7
+ type DisplayItem = {
8
+ type: "text";
9
+ text: string;
10
+ } | {
11
+ type: "toolCall";
12
+ id: string;
13
+ name: string;
14
+ args: Record<string, any>;
15
+ isError?: boolean;
16
+ };
17
+ interface SingleResult {
18
+ agent: string;
19
+ task: string;
20
+ exitCode: number;
21
+ messages: Message[];
22
+ usage: UsageStats;
23
+ model?: string;
24
+ stopReason?: string;
25
+ errorMessage?: string;
26
+ }
27
+ declare function getFinalOutput(messages: Message[]): string;
28
+ declare function getDisplayItems(messages: Message[]): DisplayItem[];
29
+ /**
30
+ * build the standard tool result for a piSpawn-based sub-agent.
31
+ * all sub-agent tools should use this instead of constructing
32
+ * return values manually — cost tagging is automatic.
33
+ */
34
+ declare function subAgentResult(text: string, details: SingleResult, isError?: boolean): {
35
+ content: {
36
+ type: "text";
37
+ text: string;
38
+ }[];
39
+ details: SingleResult & ToolCostDetails;
40
+ isError?: boolean;
41
+ };
42
+ declare function formatUsageStats(usage: {
43
+ input: number;
44
+ output: number;
45
+ cacheRead: number;
46
+ cacheWrite: number;
47
+ cost: number;
48
+ contextTokens?: number;
49
+ turns?: number;
50
+ }, model?: string): string;
51
+ declare function renderAgentTree(r: SingleResult, container: Container, showExpanded: boolean, theme: any, labelOrOpts?: string | {
52
+ label?: string;
53
+ header?: "full" | "statusOnly";
54
+ }): void;
55
+ //#endregion
56
+ export { DisplayItem, SingleResult, formatUsageStats, getDisplayItems, getFinalOutput, renderAgentTree, subAgentResult };
@@ -0,0 +1,579 @@
1
+ import { Markdown, Text, TruncatedText } from "@mariozechner/pi-tui";
2
+ import * as os from "node:os";
3
+ import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
4
+ //#region packages/core/sub-agent-render/index.ts
5
+ /**
6
+ * rendering utilities for sub-agent tool results.
7
+ *
8
+ * extracts DisplayItem, message parsing, and TUI tree rendering
9
+ * from the generic subagent extension. dedicated tools (finder,
10
+ * oracle, Task) use these for consistent renderResult display.
11
+ *
12
+ * reimplemented here because tools/ can't import from sub-agents/
13
+ * (separate nix store paths).
14
+ */
15
+ function getFinalOutput(messages) {
16
+ for (let i = messages.length - 1; i >= 0; i--) {
17
+ const msg = messages[i];
18
+ if (msg && msg.role === "assistant") {
19
+ for (const part of msg.content) if (typeof part === "object" && part !== null && "type" in part && part.type === "text") return part.text;
20
+ }
21
+ }
22
+ return "";
23
+ }
24
+ function getDisplayItems(messages) {
25
+ const errorMap = /* @__PURE__ */ new Map();
26
+ for (const msg of messages) if (msg.role === "toolResult") errorMap.set(msg.toolCallId, msg.isError);
27
+ const items = [];
28
+ for (const msg of messages) if (msg.role === "assistant") {
29
+ for (const part of msg.content) if (part.type === "text") items.push({
30
+ type: "text",
31
+ text: part.text
32
+ });
33
+ else if (part.type === "toolCall") items.push({
34
+ type: "toolCall",
35
+ id: part.id,
36
+ name: part.name,
37
+ args: part.arguments,
38
+ isError: errorMap.get(part.id)
39
+ });
40
+ }
41
+ return items;
42
+ }
43
+ /**
44
+ * build the standard tool result for a piSpawn-based sub-agent.
45
+ * all sub-agent tools should use this instead of constructing
46
+ * return values manually — cost tagging is automatic.
47
+ */
48
+ function subAgentResult(text, details, isError = false) {
49
+ return {
50
+ content: [{
51
+ type: "text",
52
+ text
53
+ }],
54
+ details: {
55
+ ...details,
56
+ cost: details.usage.cost
57
+ },
58
+ ...isError && { isError: true }
59
+ };
60
+ }
61
+ function formatTokens(count) {
62
+ if (count < 1e3) return count.toString();
63
+ if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
64
+ if (count < 1e6) return `${Math.round(count / 1e3)}k`;
65
+ return `${(count / 1e6).toFixed(1)}M`;
66
+ }
67
+ function formatUsageStats(usage, model) {
68
+ const parts = [];
69
+ if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
70
+ if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
71
+ if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
72
+ if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
73
+ if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
74
+ if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
75
+ if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
76
+ if (model) parts.push(model);
77
+ return parts.join(" ");
78
+ }
79
+ function shortenPath(p) {
80
+ const home = os.homedir();
81
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
82
+ }
83
+ function toolLabel(name) {
84
+ return name.charAt(0).toUpperCase() + name.slice(1);
85
+ }
86
+ function toolArgSummary(toolName, args) {
87
+ switch (toolName) {
88
+ case "bash": {
89
+ const command = args.cmd ?? args.command ?? "...";
90
+ return command.split("\n")[0] ?? command;
91
+ }
92
+ case "read": {
93
+ let text = shortenPath(args.file_path || args.path || "...");
94
+ const readRange = args.read_range;
95
+ const offset = args.offset;
96
+ const limit = args.limit;
97
+ if (readRange) text += `:${readRange[0]}-${readRange[1]}`;
98
+ else if (offset !== void 0 || limit !== void 0) {
99
+ const startLine = offset ?? 1;
100
+ const endLine = limit !== void 0 ? startLine + limit - 1 : "";
101
+ text += `:${startLine}${endLine ? `-${endLine}` : ""}`;
102
+ }
103
+ return text;
104
+ }
105
+ case "write": {
106
+ const rawPath = args.file_path || args.path || "...";
107
+ const lines = (args.content || "").split("\n").length;
108
+ let text = shortenPath(rawPath);
109
+ if (lines > 1) text += ` (${lines} lines)`;
110
+ return text;
111
+ }
112
+ case "edit": return shortenPath(args.file_path || args.path || "...");
113
+ case "ls": return shortenPath(args.path || ".");
114
+ case "find": return `${args.filePattern || args.pattern || "*"} in ${shortenPath(args.path || ".")}`;
115
+ case "grep": return `/${args.pattern || ""}/ in ${shortenPath(args.path || ".")}`;
116
+ default: return JSON.stringify(args);
117
+ }
118
+ }
119
+ function renderToolLine(item, fg) {
120
+ return `${item.isError === true ? fg("error", "✕") : item.isError === false ? fg("success", "✓") : fg("muted", "⋯")} ${fg("accent", toolLabel(item.name))} ${fg("dim", toolArgSummary(item.name, item.args))}`;
121
+ }
122
+ const COLLAPSED_ITEM_COUNT = 10;
123
+ function renderAgentTree(r, container, showExpanded, theme, labelOrOpts) {
124
+ const fg = theme.fg.bind(theme);
125
+ const opts = typeof labelOrOpts === "string" ? {
126
+ label: labelOrOpts,
127
+ header: "full"
128
+ } : {
129
+ label: labelOrOpts?.label,
130
+ header: labelOrOpts?.header ?? "full"
131
+ };
132
+ const MID = fg("muted", "├── ");
133
+ const END = fg("muted", "╰── ");
134
+ const CONT = fg("muted", "│ ");
135
+ const mdTheme = getMarkdownTheme();
136
+ const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
137
+ const icon = r.exitCode === -1 ? fg("warning", "⋯") : isError ? fg("error", "✕") : fg("success", "✓");
138
+ if (opts.header === "statusOnly") {
139
+ let header = icon;
140
+ if (isError && r.stopReason) header += ` ${fg("error", `[${r.stopReason}]`)}`;
141
+ container.addChild(new Text(header, 0, 0));
142
+ } else {
143
+ let header = `${icon} ${fg("toolTitle", theme.bold(opts.label ?? r.agent))}`;
144
+ if (isError && r.stopReason) header += ` ${fg("error", `[${r.stopReason}]`)}`;
145
+ container.addChild(new Text(header, 0, 0));
146
+ }
147
+ if (isError && r.errorMessage) container.addChild(new Text(MID + fg("error", `Error: ${r.errorMessage}`), 0, 0));
148
+ const toolCalls = getDisplayItems(r.messages).filter((d) => d.type === "toolCall");
149
+ const finalOutput = getFinalOutput(r.messages);
150
+ const children = [];
151
+ if (showExpanded) children.push({
152
+ kind: "text",
153
+ content: r.task
154
+ });
155
+ const visibleTools = showExpanded ? toolCalls : toolCalls.slice(-COLLAPSED_ITEM_COUNT);
156
+ const skippedTools = showExpanded ? 0 : toolCalls.length - visibleTools.length;
157
+ if (skippedTools > 0) children.push({
158
+ kind: "text",
159
+ content: `... ${skippedTools} earlier calls`
160
+ });
161
+ for (const tc of visibleTools) children.push({
162
+ kind: "tool",
163
+ item: tc
164
+ });
165
+ if (finalOutput) children.push({
166
+ kind: "summary",
167
+ output: finalOutput.trim()
168
+ });
169
+ if (children.length === 0) container.addChild(new Text(END + fg("muted", "(no output)"), 0, 0));
170
+ else for (let i = 0; i < children.length; i++) {
171
+ const child = children[i];
172
+ const isLast = i === children.length - 1;
173
+ const connector = isLast ? END : MID;
174
+ if (child.kind === "text") container.addChild(new Text(connector + fg("dim", child.content), 0, 0));
175
+ else if (child.kind === "tool") container.addChild(new TruncatedText(connector + renderToolLine(child.item, fg), 0, 0));
176
+ else if (child.kind === "summary") {
177
+ container.addChild(new Text(connector + fg("muted", "Summary:"), 0, 0));
178
+ const indent = isLast ? " " : CONT;
179
+ container.addChild(new Text(indent, 0, 0));
180
+ container.addChild(new Markdown(child.output, 0, 0, mdTheme));
181
+ }
182
+ }
183
+ if (!showExpanded && toolCalls.length > COLLAPSED_ITEM_COUNT) container.addChild(new Text(fg("muted", "(Ctrl+O to expand)"), 0, 0));
184
+ const usageStr = formatUsageStats(r.usage, r.model);
185
+ if (usageStr) container.addChild(new Text(fg("dim", usageStr), 0, 0));
186
+ }
187
+ if (import.meta.vitest) {
188
+ const { describe, it, expect } = import.meta.vitest;
189
+ describe("formatUsageStats", () => {
190
+ it("formats all fields when present", () => {
191
+ const result = formatUsageStats({
192
+ input: 1500,
193
+ output: 500,
194
+ cacheRead: 2e3,
195
+ cacheWrite: 1e3,
196
+ cost: .0023,
197
+ contextTokens: 5e3,
198
+ turns: 2
199
+ }, "gpt-4");
200
+ expect(result).toContain("2 turns");
201
+ expect(result).toContain("↑1.5k");
202
+ expect(result).toContain("↓500");
203
+ expect(result).toContain("R2.0k");
204
+ expect(result).toContain("W1.0k");
205
+ expect(result).toContain("$0.0023");
206
+ expect(result).toContain("ctx:5.0k");
207
+ expect(result).toContain("gpt-4");
208
+ });
209
+ it("omits zero/undefined fields", () => {
210
+ const result = formatUsageStats({
211
+ input: 100,
212
+ output: 50,
213
+ cacheRead: 0,
214
+ cacheWrite: 0,
215
+ cost: 0
216
+ });
217
+ expect(result).toContain("↑100");
218
+ expect(result).toContain("↓50");
219
+ expect(result).not.toContain("turn");
220
+ expect(result).not.toContain("R");
221
+ expect(result).not.toContain("W");
222
+ expect(result).not.toContain("$");
223
+ expect(result).not.toContain("ctx");
224
+ });
225
+ it("formats large token counts", () => {
226
+ expect(formatUsageStats({
227
+ input: 15e5,
228
+ output: 0,
229
+ cacheRead: 0,
230
+ cacheWrite: 0,
231
+ cost: 0
232
+ })).toContain("↑1.5M");
233
+ expect(formatUsageStats({
234
+ input: 15e3,
235
+ output: 0,
236
+ cacheRead: 0,
237
+ cacheWrite: 0,
238
+ cost: 0
239
+ })).toContain("↑15k");
240
+ expect(formatUsageStats({
241
+ input: 1500,
242
+ output: 0,
243
+ cacheRead: 0,
244
+ cacheWrite: 0,
245
+ cost: 0
246
+ })).toContain("↑1.5k");
247
+ expect(formatUsageStats({
248
+ input: 500,
249
+ output: 0,
250
+ cacheRead: 0,
251
+ cacheWrite: 0,
252
+ cost: 0
253
+ })).toContain("↑500");
254
+ });
255
+ it("handles single turn", () => {
256
+ const result = formatUsageStats({
257
+ input: 100,
258
+ output: 50,
259
+ cacheRead: 0,
260
+ cacheWrite: 0,
261
+ cost: 0,
262
+ turns: 1
263
+ });
264
+ expect(result).toContain("1 turn");
265
+ expect(result).not.toContain("1 turns");
266
+ });
267
+ it("handles plural turns", () => {
268
+ expect(formatUsageStats({
269
+ input: 100,
270
+ output: 50,
271
+ cacheRead: 0,
272
+ cacheWrite: 0,
273
+ cost: 0,
274
+ turns: 3
275
+ })).toContain("3 turns");
276
+ });
277
+ });
278
+ describe("getFinalOutput", () => {
279
+ it("returns text from last assistant message", () => {
280
+ expect(getFinalOutput([
281
+ {
282
+ role: "user",
283
+ content: [{
284
+ type: "text",
285
+ text: "hello"
286
+ }],
287
+ timestamp: 0
288
+ },
289
+ {
290
+ role: "assistant",
291
+ content: [{
292
+ type: "text",
293
+ text: "first response"
294
+ }],
295
+ api: "anthropic-messages",
296
+ provider: "anthropic",
297
+ model: "claude",
298
+ usage: {
299
+ totalTokens: 0,
300
+ cost: {
301
+ input: 0,
302
+ output: 0,
303
+ cacheRead: 0,
304
+ cacheWrite: 0,
305
+ total: 0
306
+ }
307
+ },
308
+ stopReason: "stop",
309
+ timestamp: 0
310
+ },
311
+ {
312
+ role: "user",
313
+ content: [{
314
+ type: "text",
315
+ text: "more"
316
+ }],
317
+ timestamp: 0
318
+ },
319
+ {
320
+ role: "assistant",
321
+ content: [{
322
+ type: "text",
323
+ text: "final response"
324
+ }],
325
+ api: "anthropic-messages",
326
+ provider: "anthropic",
327
+ model: "claude",
328
+ usage: {
329
+ totalTokens: 0,
330
+ cost: {
331
+ input: 0,
332
+ output: 0,
333
+ cacheRead: 0,
334
+ cacheWrite: 0,
335
+ total: 0
336
+ }
337
+ },
338
+ stopReason: "stop",
339
+ timestamp: 0
340
+ }
341
+ ])).toBe("final response");
342
+ });
343
+ it("returns empty string when no assistant messages", () => {
344
+ expect(getFinalOutput([{
345
+ role: "user",
346
+ content: [{
347
+ type: "text",
348
+ text: "hello"
349
+ }],
350
+ timestamp: 0
351
+ }])).toBe("");
352
+ });
353
+ it("skips tool calls, returns only text", () => {
354
+ expect(getFinalOutput([{
355
+ role: "assistant",
356
+ content: [{
357
+ type: "toolCall",
358
+ id: "tc1",
359
+ name: "bash",
360
+ arguments: { cmd: "ls" }
361
+ }, {
362
+ type: "text",
363
+ text: "here's the output"
364
+ }],
365
+ api: "anthropic-messages",
366
+ provider: "anthropic",
367
+ model: "claude",
368
+ usage: {
369
+ totalTokens: 0,
370
+ cost: {
371
+ input: 0,
372
+ output: 0,
373
+ cacheRead: 0,
374
+ cacheWrite: 0,
375
+ total: 0
376
+ }
377
+ },
378
+ stopReason: "stop",
379
+ timestamp: 0
380
+ }])).toBe("here's the output");
381
+ });
382
+ it("handles empty message array", () => {
383
+ expect(getFinalOutput([])).toBe("");
384
+ });
385
+ });
386
+ describe("getDisplayItems", () => {
387
+ it("extracts text and tool calls from messages", () => {
388
+ const items = getDisplayItems([
389
+ {
390
+ role: "user",
391
+ content: [{
392
+ type: "text",
393
+ text: "q"
394
+ }]
395
+ },
396
+ {
397
+ role: "assistant",
398
+ content: [{
399
+ type: "text",
400
+ text: "response"
401
+ }, {
402
+ type: "toolCall",
403
+ id: "tc1",
404
+ name: "read",
405
+ arguments: { path: "/file" }
406
+ }]
407
+ },
408
+ {
409
+ role: "toolResult",
410
+ toolCallId: "tc1",
411
+ content: [{
412
+ type: "text",
413
+ text: "file content"
414
+ }]
415
+ }
416
+ ]);
417
+ expect(items).toHaveLength(2);
418
+ expect(items[0]).toEqual({
419
+ type: "text",
420
+ text: "response"
421
+ });
422
+ expect(items[1]).toEqual({
423
+ type: "toolCall",
424
+ id: "tc1",
425
+ name: "read",
426
+ args: { path: "/file" },
427
+ isError: void 0
428
+ });
429
+ });
430
+ it("marks tool calls as error when toolResult has isError", () => {
431
+ const items = getDisplayItems([{
432
+ role: "assistant",
433
+ content: [{
434
+ type: "toolCall",
435
+ id: "tc1",
436
+ name: "bash",
437
+ arguments: { cmd: "false" }
438
+ }]
439
+ }, {
440
+ role: "toolResult",
441
+ toolCallId: "tc1",
442
+ content: [{
443
+ type: "text",
444
+ text: "error"
445
+ }],
446
+ isError: true
447
+ }]);
448
+ expect(items).toHaveLength(1);
449
+ expect(items[0]).toMatchObject({
450
+ type: "toolCall",
451
+ id: "tc1",
452
+ isError: true
453
+ });
454
+ });
455
+ it("marks tool calls as success when isError is false", () => {
456
+ expect(getDisplayItems([{
457
+ role: "assistant",
458
+ content: [{
459
+ type: "toolCall",
460
+ id: "tc1",
461
+ name: "bash",
462
+ arguments: { cmd: "true" }
463
+ }]
464
+ }, {
465
+ role: "toolResult",
466
+ toolCallId: "tc1",
467
+ content: [{
468
+ type: "text",
469
+ text: "done"
470
+ }],
471
+ isError: false
472
+ }])[0]).toMatchObject({
473
+ type: "toolCall",
474
+ id: "tc1",
475
+ isError: false
476
+ });
477
+ });
478
+ it("handles multiple assistant messages", () => {
479
+ const items = getDisplayItems([{
480
+ role: "assistant",
481
+ content: [{
482
+ type: "text",
483
+ text: "first"
484
+ }],
485
+ api: "anthropic-messages",
486
+ provider: "anthropic",
487
+ model: "claude",
488
+ usage: {
489
+ totalTokens: 0,
490
+ cost: {
491
+ input: 0,
492
+ output: 0,
493
+ cacheRead: 0,
494
+ cacheWrite: 0,
495
+ total: 0
496
+ }
497
+ },
498
+ stopReason: "stop",
499
+ timestamp: 0
500
+ }, {
501
+ role: "assistant",
502
+ content: [{
503
+ type: "text",
504
+ text: "second"
505
+ }],
506
+ api: "anthropic-messages",
507
+ provider: "anthropic",
508
+ model: "claude",
509
+ usage: {
510
+ totalTokens: 0,
511
+ cost: {
512
+ input: 0,
513
+ output: 0,
514
+ cacheRead: 0,
515
+ cacheWrite: 0,
516
+ total: 0
517
+ }
518
+ },
519
+ stopReason: "stop",
520
+ timestamp: 0
521
+ }]);
522
+ expect(items).toHaveLength(2);
523
+ expect(items[0]).toEqual({
524
+ type: "text",
525
+ text: "first"
526
+ });
527
+ expect(items[1]).toEqual({
528
+ type: "text",
529
+ text: "second"
530
+ });
531
+ });
532
+ });
533
+ describe("subAgentResult", () => {
534
+ it("builds result with cost from usage", () => {
535
+ const result = subAgentResult("found it", {
536
+ agent: "finder",
537
+ task: "search for x",
538
+ exitCode: 0,
539
+ messages: [],
540
+ usage: {
541
+ turns: 1,
542
+ input: 100,
543
+ output: 50,
544
+ cacheRead: 0,
545
+ cacheWrite: 0,
546
+ cost: .002,
547
+ contextTokens: 0
548
+ },
549
+ model: "gemini-flash"
550
+ });
551
+ expect(result.content).toEqual([{
552
+ type: "text",
553
+ text: "found it"
554
+ }]);
555
+ expect(result.details.cost).toBe(.002);
556
+ expect(result.details.model).toBe("gemini-flash");
557
+ expect(result.isError).toBeUndefined();
558
+ });
559
+ it("sets isError when passed true", () => {
560
+ expect(subAgentResult("failed", {
561
+ agent: "oracle",
562
+ task: "advise",
563
+ exitCode: 1,
564
+ messages: [],
565
+ usage: {
566
+ turns: 0,
567
+ input: 0,
568
+ output: 0,
569
+ cacheRead: 0,
570
+ cacheWrite: 0,
571
+ cost: 0,
572
+ contextTokens: 0
573
+ }
574
+ }, true).isError).toBe(true);
575
+ });
576
+ });
577
+ }
578
+ //#endregion
579
+ export { formatUsageStats, getDisplayItems, getFinalOutput, renderAgentTree, subAgentResult };
@@ -0,0 +1,17 @@
1
+ //#region packages/core/tool-cost/index.d.ts
2
+ /**
3
+ * shared cost contract for tool result details.
4
+ *
5
+ * any tool that incurs costs includes `cost: number` in its
6
+ * details object. editor/index.ts uses the type guard to narrow
7
+ * details at the session read boundary — no casting needed.
8
+ *
9
+ * sub-agent tools satisfy this via SingleResult & ToolCostDetails.
10
+ * direct-cost tools (web_search) return { cost } directly.
11
+ */
12
+ interface ToolCostDetails {
13
+ cost: number;
14
+ }
15
+ declare function hasToolCost(v: unknown): v is ToolCostDetails;
16
+ //#endregion
17
+ export { ToolCostDetails, hasToolCost };
@@ -0,0 +1,58 @@
1
+ //#region packages/core/tool-cost/index.ts
2
+ function hasToolCost(v) {
3
+ return v != null && typeof v === "object" && "cost" in v && typeof v.cost === "number";
4
+ }
5
+ if (import.meta.vitest) {
6
+ const { describe, it, expect } = import.meta.vitest;
7
+ describe("hasToolCost", () => {
8
+ it("returns true for valid ToolCostDetails", () => {
9
+ expect(hasToolCost({ cost: .0023 })).toBe(true);
10
+ });
11
+ it("returns true for object with cost number", () => {
12
+ expect(hasToolCost({ cost: 0 })).toBe(true);
13
+ expect(hasToolCost({ cost: 1.5 })).toBe(true);
14
+ expect(hasToolCost({ cost: -.001 })).toBe(true);
15
+ });
16
+ it("returns true for object with additional properties", () => {
17
+ expect(hasToolCost({
18
+ cost: .5,
19
+ turns: 3,
20
+ model: "gpt-4"
21
+ })).toBe(true);
22
+ });
23
+ it("returns false for null", () => {
24
+ expect(hasToolCost(null)).toBe(false);
25
+ });
26
+ it("returns false for undefined", () => {
27
+ expect(hasToolCost(void 0)).toBe(false);
28
+ });
29
+ it("returns false for non-objects", () => {
30
+ expect(hasToolCost("string")).toBe(false);
31
+ expect(hasToolCost(123)).toBe(false);
32
+ expect(hasToolCost(true)).toBe(false);
33
+ });
34
+ it("returns false for object without cost", () => {
35
+ expect(hasToolCost({})).toBe(false);
36
+ expect(hasToolCost({ turns: 3 })).toBe(false);
37
+ expect(hasToolCost({ model: "gpt-4" })).toBe(false);
38
+ });
39
+ it("returns false for object with non-number cost", () => {
40
+ expect(hasToolCost({ cost: "0.002" })).toBe(false);
41
+ expect(hasToolCost({ cost: null })).toBe(false);
42
+ expect(hasToolCost({ cost: void 0 })).toBe(false);
43
+ expect(hasToolCost({ cost: true })).toBe(false);
44
+ });
45
+ it("narrowing works correctly", () => {
46
+ const unknown = {
47
+ cost: .5,
48
+ turns: 2
49
+ };
50
+ if (hasToolCost(unknown)) {
51
+ const cost = unknown.cost;
52
+ expect(cost).toBe(.5);
53
+ } else expect(true).toBe(false);
54
+ });
55
+ });
56
+ }
57
+ //#endregion
58
+ export { hasToolCost };