Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, getAgentDir, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, getAgentDir, getSettingsListTheme, keyHint } from "@earendil-works/pi-coding-agent";
import { Container, Key, matchesKey, type SettingItem, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
import { Type } from "@sinclair/typebox";
import { abortable } from "./abortable.js";
Expand Down Expand Up @@ -54,6 +54,16 @@ import { FleetList, type FleetUICtx } from "./ui/fleet-list.js";
import { showSchedulesMenu } from "./ui/schedule-menu.js";
import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";

const collapseResultByStatus: Record<AgentRecord["status"], boolean> = {
queued: false,
running: false,
completed: true,
steered: true,
aborted: true,
stopped: true,
error: false,
};

// ---- Shared helpers ----

/** Tool execute return value for a text response. */
Expand Down Expand Up @@ -1452,6 +1462,21 @@ Terse command-style prompts produce shallow, generic work.
}),
),
}),
renderResult(result, { expanded, isPartial }, theme) {
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
const details = result.details as { status?: AgentRecord["status"] } | undefined;
const status = details?.status;
if (expanded || isPartial || status === undefined || !collapseResultByStatus[status]) {
return new Text(text, 0, 0);
}
return new Text(
theme.fg("dim", " ⎿ Result available (") +
keyHint("app.tools.expand", "to expand") +
theme.fg("dim", ")"),
0,
0,
);
},
execute: async (_toolCallId, params, signal, _onUpdate, _ctx) => {
const record = manager.getRecord(params.agent_id);
if (!record || record.parentAgentId) {
Expand Down Expand Up @@ -1487,6 +1512,10 @@ Terse command-style prompts produce shallow, generic work.
`Agent: ${record.id}\n` +
`Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
`Description: ${record.description}\n\n`;
const details = buildDetails(
{ displayName, description: record.description, subagentType: record.type },
record,
);

if (record.status === "running") {
output += "Agent is still running. Use wait: true or check back later.";
Expand All @@ -1510,7 +1539,7 @@ Terse command-style prompts produce shallow, generic work.
}
}

return textResult(output);
return textResult(output, details);
},
}));

Expand Down
108 changes: 108 additions & 0 deletions test/status-note-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
* a string. Drives the registered `Agent` / `get_subagent_result` tools and
* inspects the text delivered back, for a turn-limit abort and a user stop.
*/
import { initTheme } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { AgentRecord } from "../src/types.js";

vi.mock("../src/agent-runner.js", async () => {
const actual = await vi.importActual<typeof import("../src/agent-runner.js")>("../src/agent-runner.js");
Expand Down Expand Up @@ -59,6 +61,112 @@ function ctx() {

const textOf = (r: any): string => r.content[0].text;

describe("get_subagent_result rendering", () => {
afterEach(() => vi.restoreAllMocks());

it("collapses completed reports and expands the unchanged full payload", async () => {
initTheme("dark");
const payload = "first report line\nsecond report line\nthird report line";
vi.mocked(runAgent).mockResolvedValue({
responseText: payload,
session: { dispose: vi.fn() } as any,
aborted: false,
steered: false,
});
const { pi, tools } = makePi();
subagentsExtension(pi);

const spawn = await tools.get("Agent").execute(
"tc-result-render",
{ prompt: "go", description: "d", subagent_type: "general-purpose", run_in_background: true },
undefined, undefined, ctx(),
);
const id = textOf(spawn).match(/Agent ID: (\S+)/)?.[1];
expect(id, "background spawn should surface an agent id").toBeTruthy();

const result = await tools.get("get_subagent_result").execute(
"tc-result-read", { agent_id: id, wait: true }, undefined, undefined, ctx(),
);
expect(textOf(result)).toContain("Status: completed");
expect(textOf(result)).toContain(payload);

const renderer = tools.get("get_subagent_result").renderResult;
const theme = { fg: (_color: string, text: string) => text };
const collapsed = renderer(result, { expanded: false, isPartial: false }, theme).render(120).join("\n");
const expanded = renderer(result, { expanded: true, isPartial: false }, theme).render(120).join("\n");

expect(collapsed).not.toContain("second report line");
expect(collapsed).toContain("to expand");
for (const line of textOf(result).split("\n")) {
expect(expanded).toContain(line);
}
});

const rendererCases: Record<AgentRecord["status"], { collapses: boolean }> = {
queued: { collapses: false },
running: { collapses: false },
completed: { collapses: true },
steered: { collapses: true },
aborted: { collapses: true },
stopped: { collapses: true },
error: { collapses: false },
};
it.each(Object.entries(rendererCases))("renders %s status according to its classification", (status, { collapses }) => {
initTheme("dark");
const { pi, tools } = makePi();
subagentsExtension(pi);
const renderer = tools.get("get_subagent_result").renderResult;
const payload = "first report line\nsecond report line\nthird report line";
const theme = { fg: (_color: string, text: string) => text };
const rendered = renderer(
{ content: [{ type: "text", text: payload }], details: { status } },
{ expanded: false, isPartial: false },
theme,
).render(120).join("\n");

for (const line of payload.split("\n")) {
if (collapses) {
expect(rendered).not.toContain(line);
} else {
expect(rendered).toContain(line);
}
}
if (collapses) {
expect(rendered).toContain("to expand");
} else {
expect(rendered).not.toContain("to expand");
}
});

const visibleOverrides: Array<{
name: string;
details: { status: AgentRecord["status"] } | undefined;
options?: { expanded?: boolean; isPartial?: boolean };
}> = [
{ name: "missing details", details: undefined },
{ name: "partial render", details: { status: "completed" }, options: { isPartial: true } },
{ name: "expanded render", details: { status: "completed" }, options: { expanded: true } },
];
it.each(visibleOverrides)("keeps every payload line visible for $name", ({ details, options }) => {
initTheme("dark");
const { pi, tools } = makePi();
subagentsExtension(pi);
const renderer = tools.get("get_subagent_result").renderResult;
const payload = "first report line\nsecond report line\nthird report line";
const theme = { fg: (_color: string, text: string) => text };
const rendered = renderer(
{ content: [{ type: "text", text: payload }], details },
{ expanded: false, isPartial: false, ...options },
theme,
).render(120).join("\n");

for (const line of payload.split("\n")) {
expect(rendered).toContain(line);
}
expect(rendered).not.toContain("to expand");
});
});

describe("status note reaches the parent through the real handlers", () => {
afterEach(() => {
delete (globalThis as any)[Symbol.for("pi-subagents:manager")];
Expand Down
Loading