ported over subagents, minor changes to functionality, refined ui

This commit is contained in:
Amos
2026-06-27 00:19:44 +03:00
parent c6c00f89a7
commit c47a653336
17 changed files with 958 additions and 605 deletions
+198 -21
View File
@@ -15,6 +15,7 @@ import {
copySessionFile,
mergeNewEntries,
seedSubagentSessionFile,
summarizeSessionStats,
} from "../pi-extension/subagents/session.ts";
import {
@@ -475,6 +476,75 @@ describe("session.ts", () => {
assert.equal(targetLines.length, 3);
});
});
describe("summarizeSessionStats", () => {
const asstWithUsage = (id: string, opts: {
model?: string;
tools?: string[];
usage?: Record<string, unknown>;
}) => ({
type: "message",
id,
parentId: "user-001",
message: {
role: "assistant",
...(opts.model ? { model: opts.model } : {}),
content: [
{ type: "text", text: "ok" },
...(opts.tools ?? []).map((name, i) => ({ type: "toolCall", name, id: `${id}-tc${i}` })),
],
...(opts.usage ? { usage: opts.usage } : {}),
},
});
it("aggregates tokens/cost cumulatively and tracks last context size", () => {
const file = createSessionFile(dir, [
SESSION_HEADER,
{ type: "model_change", id: "mc-001", parentId: null, modelId: "claude-sonnet-4-6" },
USER_MSG,
asstWithUsage("a1", {
tools: ["read", "grep"],
usage: { input: 100, output: 50, cacheRead: 1000, cacheWrite: 200, totalTokens: 1350, cost: { total: 0.01 } },
}),
asstWithUsage("a2", {
tools: ["write"],
usage: { input: 30, output: 70, cacheRead: 2000, cacheWrite: 0, totalTokens: 3500, cost: { total: 0.02 } },
}),
]);
const stats = summarizeSessionStats(file)!;
assert.equal(stats.model, "claude-sonnet-4-6");
assert.equal(stats.toolCount, 3);
assert.equal(stats.inputTokens, 130);
assert.equal(stats.outputTokens, 120);
assert.equal(stats.cacheReadTokens, 3000);
assert.equal(stats.cacheWriteTokens, 200);
// contextTokens is the LAST assistant turn's totalTokens, not the sum.
assert.equal(stats.contextTokens, 3500);
assert.ok(Math.abs(stats.cost - 0.03) < 1e-9);
});
it("prefers per-message model over model_change", () => {
const file = createSessionFile(dir, [
SESSION_HEADER,
{ type: "model_change", id: "mc-001", parentId: null, modelId: "claude-haiku-4-5" },
asstWithUsage("a1", { model: "claude-sonnet-4-6", usage: { totalTokens: 10, cost: { total: 0 } } }),
]);
assert.equal(summarizeSessionStats(file)!.model, "claude-sonnet-4-6");
});
it("handles missing usage gracefully", () => {
const file = createSessionFile(dir, [SESSION_HEADER, USER_MSG, ASSISTANT_MSG]);
const stats = summarizeSessionStats(file)!;
assert.equal(stats.toolCount, 0);
assert.equal(stats.inputTokens, 0);
assert.equal(stats.cost, 0);
assert.equal(stats.contextTokens, 0);
});
it("returns null for an unreadable file", () => {
assert.equal(summarizeSessionStats(join(dir, "does-not-exist.jsonl")), null);
});
});
});
describe("status.ts", () => {
@@ -987,26 +1057,57 @@ describe("subagent discovery", () => {
);
});
it("bundled scout/worker/reviewer agents resolve as non-interactive; planner resolves as interactive", () => {
for (const name of ["scout", "worker", "reviewer"]) {
it("bundled scout/researcher resolve as non-interactive; worker resolves as interactive", () => {
for (const name of ["scout", "researcher"]) {
const defs = testApi.loadAgentDefaults(name);
assert.ok(defs, `expected bundled agent ${name} to be discoverable`);
assert.equal(
testApi.resolveEffectiveInteractive({ name, task: "" }, defs),
false,
`${name} should resolve as non-interactive (autonomous)`,
`${name} should resolve as non-interactive (autonomous, auto-exit)`,
);
}
const planner = testApi.loadAgentDefaults("planner");
assert.ok(planner, "expected bundled planner to be discoverable");
const worker = testApi.loadAgentDefaults("worker");
assert.ok(worker, "expected bundled worker to be discoverable");
assert.equal(
testApi.resolveEffectiveInteractive({ name: "planner", task: "" }, planner),
testApi.resolveEffectiveInteractive({ name: "worker", task: "" }, worker),
true,
"planner should resolve as interactive (no auto-exit)",
"worker should resolve as interactive (no auto-exit)",
);
});
it("worker is granted the spawning toolset restricted to scout and researcher", () => {
const worker = testApi.loadAgentDefaults("worker");
assert.ok(worker, "expected bundled worker to be discoverable");
assert.deepEqual(worker.subagentAgents, ["scout", "researcher"]);
const allowlist = testApi.buildSubagentToolAllowlist(worker.tools, { grantSpawning: true });
assert.ok(allowlist, "expected an allowlist");
const tools = new Set(allowlist!.split(","));
for (const t of ["subagent", "subagent_interrupt", "subagents_list", "subagent_resume"]) {
assert.ok(tools.has(t), `expected spawning tool ${t} in worker allowlist`);
}
assert.ok(tools.has("safe_bash"), "expected worker to keep safe_bash");
});
it("scout and researcher are not granted spawning tools", () => {
for (const name of ["scout", "researcher"]) {
const defs = testApi.loadAgentDefaults(name);
assert.ok(defs, `expected bundled agent ${name} to be discoverable`);
assert.equal(defs.subagentAgents, undefined, `${name} should not declare subagent_agents`);
}
});
it("getToolExtensionPath maps custom tools and skips built-ins", () => {
assert.equal(testApi.getToolExtensionPath("read"), undefined);
assert.equal(testApi.getToolExtensionPath("bash"), undefined);
assert.ok(testApi.getToolExtensionPath("web_search")?.endsWith("web-search/index.ts"));
assert.ok(testApi.getToolExtensionPath("safe_bash")?.endsWith("tools/safe-bash.ts"));
// Spawning tools are registered by this extension itself.
assert.ok(testApi.getToolExtensionPath("subagent")?.endsWith("index.ts"));
});
it("ignores invalid session-mode values", async () => {
await withIsolatedAgentEnv(async ({ projectAgentsDir }) => {
writeAgentFile(
@@ -1343,19 +1444,28 @@ describe("cmux.ts interpretExitSidecar", () => {
});
});
describe("commands", () => {
it("/iterate always emits a full-context fork tool call", () => {
it("/subagent emits a spawn tool call for a known agent", () => {
const { api, registeredCommands, sentUserMessages } = createMockExtensionApi();
(subagentsModule as any).default(api);
const iterate = registeredCommands.find((command) => command.name === "iterate");
assert.ok(iterate, "expected /iterate to be registered");
const subagent = registeredCommands.find((command) => command.name === "subagent");
assert.ok(subagent, "expected /subagent to be registered");
iterate.handler("Fix the bug", {});
subagent.handler("scout map the auth code", {
ui: { notify() {} },
});
assert.equal(sentUserMessages.length, 1);
assert.match(sentUserMessages[0], /fork: true/);
assert.match(sentUserMessages[0], /name: "Iterate"/);
assert.match(sentUserMessages[0], /agent: "scout"/);
assert.match(sentUserMessages[0], /map the auth code/);
});
it("does not register the removed /iterate or /plan commands", () => {
const { api, registeredCommands } = createMockExtensionApi();
(subagentsModule as any).default(api);
assert.equal(registeredCommands.find((c) => c.name === "iterate"), undefined);
assert.equal(registeredCommands.find((c) => c.name === "plan"), undefined);
});
});
@@ -1373,14 +1483,6 @@ describe("tool registration", () => {
});
});
it("expands spawning false to deny subagent interruption", () => {
const testApi = (subagentsModule as any).__test__;
const denied = testApi.resolveDenyTools({ spawning: false });
assert.equal(denied.has("subagent"), true);
assert.equal(denied.has("subagent_interrupt"), true);
assert.equal(denied.has("subagent_resume"), true);
});
it("renders partial subagent tool-call args without throwing", () => {
const { api, registeredTools } = createMockExtensionApi();
@@ -2085,6 +2187,81 @@ describe("subagents widget rendering", () => {
});
});
describe("subagent display helpers", () => {
const testApi = (subagentsModule as any).__test__;
describe("formatTokens", () => {
it("renders raw counts below 1k, 1 decimal below 10k, rounded k above", () => {
assert.equal(testApi.formatTokens(850), "850");
assert.equal(testApi.formatTokens(3200), "3.2k");
assert.equal(testApi.formatTokens(45000), "45k");
});
});
describe("contextWindowFor", () => {
it("maps known model families and returns undefined otherwise", () => {
assert.equal(testApi.contextWindowFor("claude-sonnet-4-6"), 200_000);
assert.equal(testApi.contextWindowFor("gemini-2.5-pro"), 1_000_000);
assert.equal(testApi.contextWindowFor("some-unknown-model"), undefined);
assert.equal(testApi.contextWindowFor(null), undefined);
});
});
describe("formatContextUsage", () => {
it("shows a percent gauge when the window is known", () => {
assert.equal(testApi.formatContextUsage(36_000, 200_000), "18.0%/200k");
assert.equal(testApi.formatContextUsage(500_000, 1_000_000), "50.0%/1.0M");
});
it("falls back to a window-less ctx label when unknown", () => {
assert.equal(testApi.formatContextUsage(37_000, undefined), "37k ctx");
});
});
describe("formatUsageSegments", () => {
it("emits arrow/cache/cost segments, skipping zero fields", () => {
const segs = testApi.formatUsageSegments({
model: "claude-sonnet-4-6",
toolCount: 3,
inputTokens: 3200,
outputTokens: 890,
cacheReadTokens: 45000,
cacheWriteTokens: 0,
contextTokens: 7000,
cost: 0.042,
});
assert.deepEqual(segs, ["↑3.2k", "↓890", "R45k", "$0.042"]);
});
it("returns an empty list when there is no usage", () => {
assert.deepEqual(
testApi.formatUsageSegments({
model: null,
toolCount: 0,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
contextTokens: 0,
cost: 0,
}),
[],
);
});
});
describe("widgetIcon", () => {
it("maps active/running to a glyph and waiting/starting to another", () => {
const strip = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
assert.equal(strip(testApi.widgetIcon("active")), "⟳");
assert.equal(strip(testApi.widgetIcon("running")), "⟳");
assert.equal(strip(testApi.widgetIcon("stalled")), "⟳");
assert.equal(strip(testApi.widgetIcon("waiting")), "○");
assert.equal(strip(testApi.widgetIcon("starting")), "○");
});
});
});
describe("cmux.ts", () => {
describe("shellEscape", () => {
it("wraps in single quotes", () => {