mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
1002 lines
37 KiB
TypeScript
1002 lines
37 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { act } from "preact/test-utils";
|
|
|
|
import type { DisplayItem } from "../../../../../src/features/chat/display/types";
|
|
import { topLevelDetailsSummaries } from "../../../../support/dom";
|
|
import "./setup";
|
|
import {
|
|
expectPresent,
|
|
idleTurnLifecycle,
|
|
messageStreamBlocks,
|
|
renderMessageBlockElement,
|
|
renderMessageStreamBlocksInAct,
|
|
runningTurnLifecycle,
|
|
unmountReactRootInAct,
|
|
} from "./test-helpers";
|
|
|
|
describe("work log renderer decisions", () => {
|
|
it("renders generic tool details as visible sections inside one details block", () => {
|
|
const block = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "tool-1",
|
|
kind: "tool",
|
|
role: "tool",
|
|
text: "123",
|
|
toolLabel: "github.pull_request_read",
|
|
turnId: "turn",
|
|
status: "completed",
|
|
details: [
|
|
{ title: "Arguments JSON", body: '{\n "id": 123\n}' },
|
|
{ title: "Result JSON", body: '{\n "ok": true\n}' },
|
|
],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
})[0];
|
|
|
|
const element = renderMessageBlockElement(block);
|
|
|
|
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("123");
|
|
expect(topLevelDetailsSummaries(element)).toEqual(["github.pull_request_read"]);
|
|
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["github.pull_request_read"]);
|
|
expect(element.textContent).not.toContain("Details");
|
|
expect([...element.querySelectorAll(".codex-panel__output-title")].map((title) => title.textContent)).toEqual([
|
|
"Arguments JSON",
|
|
"Result JSON",
|
|
]);
|
|
});
|
|
|
|
it("keeps the tool summary as a separate row when details are open", () => {
|
|
const block = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "cmd-1",
|
|
kind: "command",
|
|
role: "tool",
|
|
text: "npm run check",
|
|
turnId: "turn",
|
|
command: "npm run check",
|
|
cwd: "/vault",
|
|
status: "completed",
|
|
exitCode: 0,
|
|
output: "ok",
|
|
},
|
|
],
|
|
openDetails: new Set(["cmd-1:command-details"]),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
})[0];
|
|
|
|
const element = renderMessageBlockElement(block);
|
|
|
|
expect(element.classList.contains("is-open")).toBe(true);
|
|
expect(element.querySelector("details")?.hasAttribute("open")).toBe(true);
|
|
expect(topLevelDetailsSummaries(element)).toEqual(["command"]);
|
|
expect(element.querySelector(":scope > .codex-panel__tool-summary")?.textContent).toBe("npm run check");
|
|
});
|
|
|
|
it("renders generic tools without details as two compact rows", () => {
|
|
const block = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "tool-plain",
|
|
kind: "tool",
|
|
role: "tool",
|
|
text: "https://example.com",
|
|
toolLabel: "web search",
|
|
turnId: "turn",
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
})[0];
|
|
|
|
const element = renderMessageBlockElement(block);
|
|
|
|
expect(element.classList.contains("codex-panel__tool-result--plain")).toBe(true);
|
|
expect(element.querySelector(".codex-panel__tool-result-header")?.textContent).toBe("web search");
|
|
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("https://example.com");
|
|
});
|
|
|
|
it("renders path summary tools relative to the workspace root", () => {
|
|
const block = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
workspaceRoot: "/vault/project",
|
|
displayItems: [
|
|
{
|
|
id: "tool-path",
|
|
kind: "tool",
|
|
role: "tool",
|
|
text: "/vault/project/assets/image.png",
|
|
toolLabel: "imageView",
|
|
summaryPath: true,
|
|
turnId: "turn",
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
})[0];
|
|
|
|
const element = renderMessageBlockElement(block);
|
|
|
|
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("assets/image.png");
|
|
expect(element.querySelector(".codex-panel__tool-summary")?.getAttribute("title")).toBeNull();
|
|
});
|
|
|
|
it("updates path summary tools when the workspace root changes", () => {
|
|
const parent = document.createElement("div");
|
|
const item = {
|
|
id: "tool-path",
|
|
kind: "tool",
|
|
role: "tool",
|
|
text: "/vault/project/assets/image.png",
|
|
toolLabel: "imageView",
|
|
summaryPath: true,
|
|
turnId: "turn",
|
|
} as const;
|
|
const baseContext = {
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [item] satisfies DisplayItem[],
|
|
openDetails: new Set<string>(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (element: HTMLElement, text: string) => element.createDiv({ text }),
|
|
renderTextWithWikiLinks: (element: HTMLElement, text: string) => element.createDiv({ text }),
|
|
};
|
|
|
|
renderMessageStreamBlocksInAct(parent, messageStreamBlocks({ ...baseContext, workspaceRoot: "/vault" }));
|
|
expect(parent.querySelector(".codex-panel__tool-summary")?.textContent).toBe("project/assets/image.png");
|
|
|
|
renderMessageStreamBlocksInAct(parent, messageStreamBlocks({ ...baseContext, workspaceRoot: "/vault/project" }));
|
|
expect(parent.querySelector(".codex-panel__tool-summary")?.textContent).toBe("assets/image.png");
|
|
unmountReactRootInAct(parent);
|
|
});
|
|
|
|
it("keeps path summary tools absolute outside the workspace root", () => {
|
|
const block = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
workspaceRoot: "/vault/project",
|
|
displayItems: [
|
|
{
|
|
id: "tool-path",
|
|
kind: "tool",
|
|
role: "tool",
|
|
text: "/tmp/image.png",
|
|
toolLabel: "imageView",
|
|
summaryPath: true,
|
|
turnId: "turn",
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
})[0];
|
|
|
|
const element = renderMessageBlockElement(block);
|
|
|
|
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("/tmp/image.png");
|
|
});
|
|
|
|
it("does not treat generic tool summaries as paths without an explicit marker", () => {
|
|
const block = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
workspaceRoot: "/vault/project",
|
|
displayItems: [
|
|
{
|
|
id: "tool-path-like",
|
|
kind: "tool",
|
|
role: "tool",
|
|
text: "/vault/project",
|
|
toolLabel: "example.tool",
|
|
turnId: "turn",
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
})[0];
|
|
|
|
const element = renderMessageBlockElement(block);
|
|
|
|
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("/vault/project");
|
|
});
|
|
|
|
it("renders hook metadata as rows inside one details block", () => {
|
|
const block = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "hook-1",
|
|
kind: "hook",
|
|
role: "tool",
|
|
text: "postToolUse: Formatted 1 file.",
|
|
toolLabel: "hook",
|
|
turnId: "turn",
|
|
status: "completed",
|
|
details: [
|
|
{
|
|
rows: [
|
|
{ key: "status", value: "completed" },
|
|
{ key: "event", value: "postToolUse" },
|
|
{ key: "message", value: "Formatted 1 file." },
|
|
{ key: "duration", value: "1ms" },
|
|
],
|
|
},
|
|
{ title: "Hook output", body: "feedback: ok" },
|
|
],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
})[0];
|
|
|
|
const element = renderMessageBlockElement(block);
|
|
|
|
expect(topLevelDetailsSummaries(element)).toEqual(["hook"]);
|
|
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["hook"]);
|
|
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("postToolUse: Formatted 1 file.");
|
|
expect(element.textContent).not.toContain("Details");
|
|
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("statuscompleted");
|
|
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("eventpostToolUse");
|
|
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("messageFormatted 1 file.");
|
|
expect([...element.querySelectorAll(".codex-panel__output-title")].map((title) => title.textContent)).toEqual(["Hook output"]);
|
|
expect(element.querySelector(".codex-panel__output pre")?.textContent).toBe("feedback: ok");
|
|
});
|
|
|
|
it("renders hook metadata when the hook is inside a completed-turn activity group", () => {
|
|
const blocks = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: idleTurnLifecycle(),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{ id: "u1", kind: "message", role: "user", text: "do it", turnId: "turn", markdown: true },
|
|
{
|
|
id: "hook-1",
|
|
kind: "hook",
|
|
role: "tool",
|
|
text: "postToolUse: Formatted 1 file.",
|
|
toolLabel: "hook",
|
|
turnId: "turn",
|
|
status: "completed",
|
|
details: [
|
|
{
|
|
rows: [
|
|
{ key: "status", value: "completed" },
|
|
{ key: "event", value: "postToolUse" },
|
|
{ key: "message", value: "Formatted 1 file." },
|
|
],
|
|
},
|
|
{ title: "Hook output", body: "feedback: ok" },
|
|
],
|
|
},
|
|
{ id: "a1", kind: "message", role: "assistant", text: "done", turnId: "turn", markdown: true },
|
|
],
|
|
openDetails: new Set(["turn:turn:activity", "hook-1"]),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
});
|
|
|
|
const element = renderMessageBlockElement(expectPresent(blocks.find((block) => block.key === "activity:turn-turn-activity")));
|
|
|
|
expect(element).toBeDefined();
|
|
expect(element.querySelector(":scope > summary")?.textContent).toBe("Work details: hook");
|
|
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("postToolUse: Formatted 1 file.");
|
|
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("statuscompleted");
|
|
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("eventpostToolUse");
|
|
expect(element.querySelector(".codex-panel__output-title")?.textContent).toBe("Hook output");
|
|
expect(element.querySelector(".codex-panel__output pre")?.textContent).toBe("feedback: ok");
|
|
});
|
|
|
|
it("keeps completed-turn activity group items mounted through React", () => {
|
|
const parent = document.createElement("div");
|
|
const onDetailsToggle = vi.fn();
|
|
|
|
renderMessageStreamBlocksInAct(
|
|
parent,
|
|
messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: idleTurnLifecycle(),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{ id: "u1", kind: "message", role: "user", text: "do it", turnId: "turn", markdown: true },
|
|
{
|
|
id: "hook-1",
|
|
kind: "hook",
|
|
role: "tool",
|
|
text: "postToolUse: Formatted 1 file.",
|
|
toolLabel: "hook",
|
|
turnId: "turn",
|
|
status: "completed",
|
|
details: [
|
|
{
|
|
rows: [
|
|
{ key: "status", value: "completed" },
|
|
{ key: "event", value: "postToolUse" },
|
|
],
|
|
},
|
|
{ title: "Hook output", body: "feedback: ok" },
|
|
],
|
|
},
|
|
{
|
|
id: "agent-1",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Spawn agent",
|
|
turnId: "turn",
|
|
tool: "spawnAgent",
|
|
status: "completed",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["child"],
|
|
prompt: null,
|
|
model: null,
|
|
reasoningEffort: null,
|
|
agents: [{ threadId: "child", status: "completed", message: "Done" }],
|
|
},
|
|
{ id: "a1", kind: "message", role: "assistant", text: "done", turnId: "turn", markdown: true },
|
|
],
|
|
openDetails: new Set(["turn:turn:activity", "hook-1:details"]),
|
|
onDetailsToggle,
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (element, text) => element.createDiv({ text }),
|
|
renderTextWithWikiLinks: (element, text) => element.createDiv({ text }),
|
|
}),
|
|
);
|
|
|
|
const group = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="activity:turn-turn-activity"]'));
|
|
expect(group.querySelector(":scope > .codex-panel__activity-group > summary")?.textContent).toBe("Work details: agent, hook");
|
|
expect(group.querySelector(".codex-panel__tool-result .codex-panel__tool-result-header")?.textContent).toBe("hook");
|
|
expect(group.querySelector(".codex-panel__tool-result > .codex-panel__tool-summary")?.textContent).toBe(
|
|
"postToolUse: Formatted 1 file.",
|
|
);
|
|
expect(group.querySelector(".codex-panel__agent-activity .codex-panel__tool-summary")?.textContent).toBe("spawn child (completed)");
|
|
|
|
const details = expectPresent(group.querySelector<HTMLDetailsElement>(".codex-panel__activity-group"));
|
|
void act(() => {
|
|
details.open = false;
|
|
details.dispatchEvent(new Event("toggle", { bubbles: false }));
|
|
});
|
|
|
|
expect(onDetailsToggle).toHaveBeenCalledWith("turn:turn:activity", false);
|
|
unmountReactRootInAct(parent);
|
|
});
|
|
|
|
it("renders task progress items as a dedicated task list", () => {
|
|
const block = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "plan-progress-turn",
|
|
kind: "taskProgress",
|
|
role: "tool",
|
|
text: "Plan\n[>] Patch UI",
|
|
turnId: "turn",
|
|
explanation: "Plan",
|
|
steps: [
|
|
{ step: "Inspect code", status: "completed" },
|
|
{ step: "Patch UI", status: "inProgress" },
|
|
],
|
|
status: "inProgress",
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
})[0];
|
|
|
|
const element = renderMessageBlockElement(block);
|
|
|
|
expect(element.classList.contains("codex-panel__work-message")).toBe(true);
|
|
expect(element.classList.contains("codex-panel__task-progress")).toBe(true);
|
|
expect(element.querySelector(".codex-panel__message-role")?.textContent).toBe("tasks");
|
|
expect(element.textContent).toContain("[x]Inspect code");
|
|
expect(element.textContent).toContain("[>]Patch UI");
|
|
});
|
|
|
|
it("keeps task progress React items mounted in the message stream host", () => {
|
|
const parent = document.createElement("div");
|
|
|
|
renderMessageStreamBlocksInAct(
|
|
parent,
|
|
messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "plan-progress-turn",
|
|
kind: "taskProgress",
|
|
role: "tool",
|
|
text: "Plan\n[>] Patch UI",
|
|
turnId: "turn",
|
|
explanation: "Plan",
|
|
steps: [
|
|
{ step: "Inspect code", status: "completed" },
|
|
{ step: "Patch UI", status: "inProgress" },
|
|
],
|
|
status: "inProgress",
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (element, text) => element.createDiv({ text }),
|
|
renderTextWithWikiLinks: (element, text) => element.createDiv({ text }),
|
|
}),
|
|
);
|
|
|
|
const block = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="live-task:plan-progress-turn"]'));
|
|
expect(block.querySelector(".codex-panel__task-progress .codex-panel__message-role")?.textContent).toBe("tasks");
|
|
expect(block.textContent).toContain("[x]Inspect code");
|
|
expect(block.textContent).toContain("[>]Patch UI");
|
|
unmountReactRootInAct(parent);
|
|
});
|
|
|
|
it("renders active task progress with the shared bottom live blocks", () => {
|
|
const blocks = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{ id: "u1", kind: "message", role: "user", text: "Do it", turnId: "turn", markdown: true },
|
|
{
|
|
id: "plan-progress-turn",
|
|
kind: "taskProgress",
|
|
role: "tool",
|
|
text: "[>] Patch UI",
|
|
turnId: "turn",
|
|
explanation: null,
|
|
steps: [{ step: "Patch UI", status: "inProgress" }],
|
|
status: "inProgress",
|
|
},
|
|
{ id: "a1", kind: "message", role: "assistant", text: "Working", turnId: "turn", markdown: true },
|
|
{
|
|
id: "agent-1",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Wait for agent",
|
|
turnId: "turn",
|
|
tool: "wait",
|
|
status: "running",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["running"],
|
|
prompt: null,
|
|
model: null,
|
|
reasoningEffort: null,
|
|
agents: [{ threadId: "running", status: "running", message: "Inspecting renderer" }],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
pendingRequestsSignature: "request:1",
|
|
renderPendingRequests: () => "Request",
|
|
});
|
|
|
|
expect(blocks.map((block) => block.key)).toEqual([
|
|
"item:u1",
|
|
"item:a1",
|
|
"item:agent-1",
|
|
"live-task:plan-progress-turn",
|
|
"live-agents:turn",
|
|
"pending-requests",
|
|
]);
|
|
});
|
|
|
|
it("orders shared bottom live blocks by insertion order", () => {
|
|
const blocks = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{ id: "u1", kind: "message", role: "user", text: "Do it", turnId: "turn", markdown: true },
|
|
{
|
|
id: "agent-1",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Wait for agent",
|
|
turnId: "turn",
|
|
tool: "wait",
|
|
status: "running",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["running"],
|
|
prompt: null,
|
|
model: null,
|
|
reasoningEffort: null,
|
|
agents: [{ threadId: "running", status: "running", message: null }],
|
|
},
|
|
{
|
|
id: "plan-progress-turn",
|
|
kind: "taskProgress",
|
|
role: "tool",
|
|
text: "[>] Patch UI",
|
|
turnId: "turn",
|
|
explanation: null,
|
|
steps: [{ step: "Patch UI", status: "inProgress" }],
|
|
status: "inProgress",
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
});
|
|
|
|
expect(blocks.map((block) => block.key)).toEqual(["item:u1", "item:agent-1", "live-agents:turn", "live-task:plan-progress-turn"]);
|
|
});
|
|
|
|
it("anchors the live agent summary at the first agent activity", () => {
|
|
const blocks = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{ id: "u1", kind: "message", role: "user", text: "Do it", turnId: "turn", markdown: true },
|
|
{
|
|
id: "agent-spawn",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Spawn agent",
|
|
turnId: "turn",
|
|
tool: "spawnAgent",
|
|
status: "completed",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["child"],
|
|
prompt: "Inspect the renderer.",
|
|
model: null,
|
|
reasoningEffort: null,
|
|
agents: [{ threadId: "child", status: "completed", message: null }],
|
|
},
|
|
{
|
|
id: "plan-progress-turn",
|
|
kind: "taskProgress",
|
|
role: "tool",
|
|
text: "[>] Patch UI",
|
|
turnId: "turn",
|
|
explanation: null,
|
|
steps: [{ step: "Patch UI", status: "inProgress" }],
|
|
status: "inProgress",
|
|
},
|
|
{
|
|
id: "agent-wait",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Wait for agent",
|
|
turnId: "turn",
|
|
tool: "wait",
|
|
status: "running",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["child"],
|
|
prompt: null,
|
|
model: null,
|
|
reasoningEffort: null,
|
|
agents: [{ threadId: "child", status: "running", message: "Inspecting renderer" }],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
});
|
|
|
|
expect(blocks.map((block) => block.key)).toEqual([
|
|
"item:u1",
|
|
"item:agent-spawn",
|
|
"item:agent-wait",
|
|
"live-agents:turn",
|
|
"live-task:plan-progress-turn",
|
|
]);
|
|
});
|
|
|
|
it("renders agent activity as a one-line summary with consolidated details", () => {
|
|
const block = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "agent-1",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Spawn agent",
|
|
turnId: "turn",
|
|
tool: "spawnAgent",
|
|
status: "completed",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["child"],
|
|
prompt: "Inspect the renderer.",
|
|
model: "gpt-5.5",
|
|
reasoningEffort: "high",
|
|
agents: [{ threadId: "child", status: "completed", message: "Done" }],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
})[0];
|
|
|
|
const element = renderMessageBlockElement(block);
|
|
|
|
expect(element.classList.contains("codex-panel__work-message")).toBe(true);
|
|
expect(element.classList.contains("codex-panel__agent-activity")).toBe(true);
|
|
expect(element.querySelector(".codex-panel__message-role")?.textContent).toBe("agent");
|
|
const summary = expectPresent(element.querySelector<HTMLElement>(".codex-panel__tool-summary"));
|
|
expect(summary.textContent).toBe("spawn child: Inspect the renderer. (completed)");
|
|
expect(summary.classList.contains("codex-panel__agent-activity-summary")).toBe(true);
|
|
expect([...element.querySelectorAll("details summary")].map((detailsSummary) => detailsSummary.textContent)).toEqual(["Details"]);
|
|
expect(element.textContent).toContain("targetchild");
|
|
expect(element.textContent).toContain("PromptInspect the renderer.");
|
|
expect(element.textContent).toContain("childcompleted: Done");
|
|
});
|
|
|
|
it("keeps agent React details mounted in the message stream host", () => {
|
|
const parent = document.createElement("div");
|
|
const onDetailsToggle = vi.fn();
|
|
|
|
renderMessageStreamBlocksInAct(
|
|
parent,
|
|
messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "agent-1",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Spawn agent",
|
|
turnId: "turn",
|
|
tool: "spawnAgent",
|
|
status: "completed",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["child"],
|
|
prompt: "Inspect the renderer.",
|
|
model: "gpt-5.5",
|
|
reasoningEffort: "high",
|
|
agents: [{ threadId: "child", status: "completed", message: "Done" }],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
onDetailsToggle,
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (element, text) => element.createDiv({ text }),
|
|
renderTextWithWikiLinks: (element, text) => element.createDiv({ text }),
|
|
}),
|
|
);
|
|
|
|
const block = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:agent-1"]'));
|
|
expect(block.querySelector(".codex-panel__agent-activity .codex-panel__message-role")?.textContent).toBe("agent");
|
|
expect(block.querySelector(".codex-panel__tool-summary")?.textContent).toBe("spawn child: Inspect the renderer. (completed)");
|
|
expect([...block.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["Details"]);
|
|
|
|
const details = expectPresent(block.querySelector<HTMLDetailsElement>("details"));
|
|
void act(() => {
|
|
details.open = true;
|
|
details.dispatchEvent(new Event("toggle", { bubbles: false }));
|
|
});
|
|
|
|
expect(onDetailsToggle).toHaveBeenCalledWith("agent-1:agent-details", true);
|
|
expect(expectPresent(block.querySelector(".codex-panel__agent-activity")).classList.contains("is-open")).toBe(true);
|
|
unmountReactRootInAct(parent);
|
|
});
|
|
|
|
it("keeps agent activity prompt previews visually constrained to one line", () => {
|
|
const block = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "agent-1",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Spawn agent",
|
|
turnId: "turn",
|
|
tool: "spawnAgent",
|
|
status: "running",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["child"],
|
|
prompt: `Inspect the renderer.\n${"a".repeat(180)}`,
|
|
model: null,
|
|
reasoningEffort: null,
|
|
agents: [],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
})[0];
|
|
|
|
const element = renderMessageBlockElement(block);
|
|
const summary = expectPresent(element.querySelector<HTMLElement>(".codex-panel__agent-activity-summary"));
|
|
|
|
expect(summary.textContent).toBe(`spawn child: Inspect the renderer. ${"a".repeat(73)}... (running)`);
|
|
});
|
|
|
|
it("collapses long agent output away from the agent status row", () => {
|
|
const longMessage = `Done\n${"a".repeat(180)}`;
|
|
const threadId = "019e061e-0046-7653-a362-86de9a47cb5c";
|
|
const onDetailsToggle = vi.fn();
|
|
const block = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "agent-1",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Wait for agent",
|
|
turnId: "turn",
|
|
tool: "wait",
|
|
status: "completed",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: [threadId],
|
|
prompt: null,
|
|
model: null,
|
|
reasoningEffort: null,
|
|
agents: [{ threadId, status: "completed", message: longMessage }],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
onDetailsToggle,
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
})[0];
|
|
|
|
const element = renderMessageBlockElement(block);
|
|
|
|
expect(element.querySelector(".codex-panel__agent-thread")?.textContent).toBe("019e061e");
|
|
expect(element.querySelector(".codex-panel__agent-status")?.textContent).toBe("completed: Done");
|
|
expect(element.querySelector(".codex-panel__agent-status")?.textContent).not.toContain("a".repeat(180));
|
|
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["Details"]);
|
|
expect(element.textContent).toContain("Agent output 019e061e");
|
|
expect(element.textContent).toContain(longMessage);
|
|
const details = element.querySelector("details");
|
|
expect(details?.hasAttribute("open")).toBe(false);
|
|
details?.dispatchEvent(new Event("toggle"));
|
|
expect(onDetailsToggle).toHaveBeenCalled();
|
|
});
|
|
|
|
it("renders a compact live agent summary while subagents are running", () => {
|
|
const blocks = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "agent-1",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Wait for agent",
|
|
turnId: "turn",
|
|
tool: "wait",
|
|
status: "running",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["done", "running"],
|
|
prompt: null,
|
|
model: null,
|
|
reasoningEffort: null,
|
|
agents: [
|
|
{ threadId: "done", status: "completed", message: null },
|
|
{ threadId: "running", status: "running", message: "Inspecting renderer" },
|
|
],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
});
|
|
|
|
const summary = renderMessageBlockElement(expectPresent(blocks.at(-1)));
|
|
|
|
expect(summary.classList.contains("codex-panel__work-message")).toBe(true);
|
|
expect(summary.classList.contains("codex-panel__agent-summary")).toBe(true);
|
|
expect(summary.textContent).toContain("agents");
|
|
expect(summary.textContent).toContain("Agents 1 running, 1 done");
|
|
expect(summary.textContent).toContain("runningrunning: Inspecting renderer");
|
|
expect(summary.textContent).not.toContain("donecompleted");
|
|
});
|
|
|
|
it("renders the compact live agent summary as a React block", () => {
|
|
const parent = document.createElement("div");
|
|
|
|
renderMessageStreamBlocksInAct(
|
|
parent,
|
|
messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "agent-1",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Wait for agent",
|
|
turnId: "turn",
|
|
tool: "wait",
|
|
status: "running",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["done", "running"],
|
|
prompt: null,
|
|
model: null,
|
|
reasoningEffort: null,
|
|
agents: [
|
|
{ threadId: "done", status: "completed", message: null },
|
|
{ threadId: "running", status: "running", message: "Inspecting renderer" },
|
|
],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (element, text) => element.createDiv({ text }),
|
|
renderTextWithWikiLinks: (element, text) => element.createDiv({ text }),
|
|
}),
|
|
);
|
|
|
|
const summary = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="live-agents:turn"]'));
|
|
expect(summary.querySelector(".codex-panel__agent-summary")?.textContent).toContain("Agents 1 running, 1 done");
|
|
expect(summary.textContent).toContain("runningrunning: Inspecting renderer");
|
|
unmountReactRootInAct(parent);
|
|
});
|
|
|
|
it("renders active reasoning as a React message stream block", () => {
|
|
const parent = document.createElement("div");
|
|
|
|
renderMessageStreamBlocksInAct(
|
|
parent,
|
|
messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [{ id: "reasoning-1", kind: "reasoning", role: "tool", text: "", turnId: "turn", status: "running" }],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (element, text) => element.createDiv({ text }),
|
|
renderTextWithWikiLinks: (element, text) => element.createDiv({ text }),
|
|
}),
|
|
);
|
|
|
|
const reasoning = expectPresent(
|
|
parent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:reasoning-1"] .codex-panel__reasoning'),
|
|
);
|
|
expect(reasoning.classList.contains("is-active")).toBe(true);
|
|
expect(reasoning.querySelector(".codex-panel__reasoning-role")?.textContent).toBe("reasoning");
|
|
expect(reasoning.querySelector(".codex-panel__reasoning-content")?.textContent).toBe("Reasoning...");
|
|
unmountReactRootInAct(parent);
|
|
});
|
|
|
|
it("hides the live agent summary once all subagents are complete", () => {
|
|
const blocks = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "agent-1",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Wait for agent",
|
|
turnId: "turn",
|
|
tool: "wait",
|
|
status: "completed",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["done"],
|
|
prompt: null,
|
|
model: null,
|
|
reasoningEffort: null,
|
|
agents: [{ threadId: "done", status: "completed", message: null }],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
});
|
|
|
|
expect(blocks.some((block) => block.key.startsWith("live-agents:"))).toBe(false);
|
|
});
|
|
|
|
it("marks the live agent summary failed when any subagent fails", () => {
|
|
const blocks = messageStreamBlocks({
|
|
activeThreadId: "thread",
|
|
turnLifecycle: runningTurnLifecycle("turn"),
|
|
historyCursor: null,
|
|
loadingHistory: false,
|
|
displayItems: [
|
|
{
|
|
id: "agent-1",
|
|
kind: "agent",
|
|
role: "tool",
|
|
text: "Wait for agent",
|
|
turnId: "turn",
|
|
tool: "wait",
|
|
status: "completed",
|
|
senderThreadId: "parent",
|
|
receiverThreadIds: ["failed", "running"],
|
|
prompt: null,
|
|
model: null,
|
|
reasoningEffort: null,
|
|
agents: [
|
|
{ threadId: "failed", status: "errored", message: "Failed" },
|
|
{ threadId: "running", status: "running", message: null },
|
|
],
|
|
},
|
|
],
|
|
openDetails: new Set(),
|
|
loadOlderTurns: vi.fn(),
|
|
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
|
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
|
});
|
|
|
|
const summary = renderMessageBlockElement(expectPresent(blocks.at(-1)));
|
|
|
|
expect(summary.classList.contains("codex-panel__execution--failed")).toBe(true);
|
|
expect(summary.textContent).toContain("Agents 1 failed, 1 running");
|
|
expect(summary.textContent).toContain("runningrunning");
|
|
expect(summary.textContent).not.toContain("failederrored: Failed");
|
|
});
|
|
});
|