Show context compaction as a work item

This commit is contained in:
murashit 2026-06-06 22:45:20 +09:00
parent 49c7fc94fd
commit 1430990309
7 changed files with 73 additions and 7 deletions

View file

@ -167,6 +167,7 @@ function turnActivitySummary(items: readonly DisplayItem[]): string {
countMatchingLabel(items, (item) => item.kind === "tool" && !isSteeringActivityDisplayItem(item), "tool"),
countLabel(items, "hook", "hook"),
countLabel(items, "reasoning", "thought", "thought notes"),
countLabel(items, "contextCompaction", "context compaction"),
countLabel(items, "approvalResult", "approval"),
countLabel(items, "userInputResult", "input"),
countLabel(items, "reviewResult", "review"),

View file

@ -393,10 +393,9 @@ function reviewModeDisplayItem(item: ReviewModeItem, turnId?: string): DisplayIt
function contextCompactionDisplayItem(item: ContextCompactionItem, turnId?: string): DisplayItem {
return {
id: item.id,
kind: "tool",
kind: "contextCompaction",
role: "tool",
text: "Context compaction",
toolLabel: "contextCompaction",
...definedProp("turnId", turnId),
itemId: item.id,
};

View file

@ -9,6 +9,7 @@ export type DisplayKind =
| "agent"
| "hook"
| "reasoning"
| "contextCompaction"
| "system"
| "goal"
| "approvalResult"
@ -162,6 +163,11 @@ export interface ReasoningDisplayItem extends ToolDisplayBase {
kind: "reasoning";
}
export interface ContextCompactionDisplayItem extends DisplayBase {
kind: "contextCompaction";
role: "tool";
}
export interface TaskProgressStep {
step: string;
status: "pending" | "inProgress" | "completed";
@ -218,6 +224,7 @@ export type DisplayItem =
| ToolCallDisplayItem
| HookDisplayItem
| ReasoningDisplayItem
| ContextCompactionDisplayItem
| TaskProgressDisplayItem
| AgentDisplayItem
| ApprovalResultDisplayItem

View file

@ -68,7 +68,7 @@ function isRenderableToolResultItem(item: DisplayItem): item is ToolResultDispla
}
function isRenderableWorkItem(item: DisplayItem): item is WorkItemDisplayItem {
return item.kind === "taskProgress" || item.kind === "agent" || item.kind === "reasoning";
return item.kind === "taskProgress" || item.kind === "agent" || item.kind === "reasoning" || item.kind === "contextCompaction";
}
function displayItemNode(item: DisplayItem, context: MessageStreamContext): UiNode {

View file

@ -7,6 +7,7 @@ import type {
AgentDisplayItem,
AgentRunSummary,
AgentRunSummaryAgent,
ContextCompactionDisplayItem,
DisplayItem,
ReasoningDisplayItem,
TaskProgressDisplayItem,
@ -19,7 +20,7 @@ import { shortThreadId, truncate } from "../../../utils";
const AGENT_ROW_MESSAGE_PREVIEW_LIMIT = 120;
const AGENT_ACTIVITY_PROMPT_PREVIEW_LIMIT = 96;
export type WorkItemDisplayItem = TaskProgressDisplayItem | AgentDisplayItem | ReasoningDisplayItem;
export type WorkItemDisplayItem = TaskProgressDisplayItem | AgentDisplayItem | ReasoningDisplayItem | ContextCompactionDisplayItem;
export interface WorkItemContext {
turnLifecycle: ChatTurnLifecycleState;
@ -43,6 +44,7 @@ export function agentRunSummaryNode(summary: AgentRunSummary): UiNode {
export function workItemNode(item: WorkItemDisplayItem, context: WorkItemContext): UiNode {
if (item.kind === "taskProgress") return <TaskProgressItem item={item} />;
if (item.kind === "agent") return <AgentItem item={item} context={context} />;
if (item.kind === "contextCompaction") return <ContextCompactionItem item={item} context={context} />;
return <ReasoningItem item={item} context={context} />;
}
@ -75,6 +77,15 @@ function TaskProgressItem({ item }: { item: TaskProgressDisplayItem }): UiNode {
);
}
function ContextCompactionItem({ item, context }: { item: ContextCompactionDisplayItem; context: WorkItemContext }): UiNode {
const active = workItemsActiveTurnId(context) === item.turnId;
return (
<WorkMessage label="context" className="codex-panel__context-compaction" state={active ? "running" : "completed"}>
<div className="codex-panel__tool-summary">{active ? "Compacting context..." : "Context compacted"}</div>
</WorkMessage>
);
}
function AgentItem({ item, context }: { item: AgentDisplayItem; context: WorkItemContext }): UiNode {
const detailsKey = `${item.id}:agent-details`;
const [detailsOpen, setDetailsOpen] = useState(context.openDetails.has(detailsKey));

View file

@ -813,13 +813,12 @@ describe("thread item conversion preserves app-server semantics", () => {
});
});
it("preserves context compaction items as short tool items", () => {
it("preserves context compaction items as short work items", () => {
const item: ThreadItem = { type: "contextCompaction", id: "compact-1" };
expect(displayItemFromThreadItem(item, "t1")).toMatchObject({
kind: "tool",
kind: "contextCompaction",
text: "Context compaction",
toolLabel: "contextCompaction",
turnId: "t1",
itemId: "compact-1",
});

View file

@ -961,6 +961,55 @@ describe("work log renderer decisions", () => {
unmountUiRootInAct(parent);
});
it("renders context compaction as a one-line work item while running and after completion", () => {
const runningParent = document.createElement("div");
const item: DisplayItem = { id: "compact-1", kind: "contextCompaction", role: "tool", text: "Context compaction", turnId: "turn" };
renderMessageStreamBlocksInAct(
runningParent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: runningTurnLifecycle("turn"),
historyCursor: null,
loadingHistory: false,
displayItems: [item],
openDetails: new Set(),
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
}),
);
const running = expectPresent(
runningParent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:compact-1"] .codex-panel__context-compaction'),
);
expect(running.querySelector(".codex-panel__message-role")?.textContent).toBe("context");
expect(running.querySelector(".codex-panel__tool-summary")?.textContent).toBe("Compacting context...");
expect(running.classList.contains("codex-panel__execution--running")).toBe(true);
unmountUiRootInAct(runningParent);
const completedParent = document.createElement("div");
renderMessageStreamBlocksInAct(
completedParent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: idleTurnLifecycle(),
historyCursor: null,
loadingHistory: false,
displayItems: [item],
openDetails: new Set(),
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
}),
);
const completed = expectPresent(
completedParent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:compact-1"] .codex-panel__context-compaction'),
);
expect(completed.querySelector(".codex-panel__tool-summary")?.textContent).toBe("Context compacted");
expect(completed.classList.contains("codex-panel__execution--completed")).toBe(true);
unmountUiRootInAct(completedParent);
});
it("hides the live agent summary once all subagents are complete", () => {
const blocks = messageStreamBlocks({
activeThreadId: "thread",