From 1430990309952fae2fdaf9b5ea9aa97ca2033f20 Mon Sep 17 00:00:00 2001 From: murashit Date: Sat, 6 Jun 2026 22:45:20 +0900 Subject: [PATCH] Show context compaction as a work item --- src/features/chat/display/blocks.ts | 1 + src/features/chat/display/thread-items.ts | 3 +- src/features/chat/display/types.ts | 7 +++ src/features/chat/ui/message-stream.tsx | 2 +- src/features/chat/ui/work-items.tsx | 13 ++++- .../chat/display/display-model.test.ts | 5 +- .../chat/ui/message-stream/work-log.test.tsx | 49 +++++++++++++++++++ 7 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/features/chat/display/blocks.ts b/src/features/chat/display/blocks.ts index 5d73abdb..d76c5df0 100644 --- a/src/features/chat/display/blocks.ts +++ b/src/features/chat/display/blocks.ts @@ -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"), diff --git a/src/features/chat/display/thread-items.ts b/src/features/chat/display/thread-items.ts index 12438e2f..f89da6df 100644 --- a/src/features/chat/display/thread-items.ts +++ b/src/features/chat/display/thread-items.ts @@ -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, }; diff --git a/src/features/chat/display/types.ts b/src/features/chat/display/types.ts index 382019fd..993879a5 100644 --- a/src/features/chat/display/types.ts +++ b/src/features/chat/display/types.ts @@ -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 diff --git a/src/features/chat/ui/message-stream.tsx b/src/features/chat/ui/message-stream.tsx index 179c28da..a57dd800 100644 --- a/src/features/chat/ui/message-stream.tsx +++ b/src/features/chat/ui/message-stream.tsx @@ -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 { diff --git a/src/features/chat/ui/work-items.tsx b/src/features/chat/ui/work-items.tsx index f10b3e07..8c68c4bc 100644 --- a/src/features/chat/ui/work-items.tsx +++ b/src/features/chat/ui/work-items.tsx @@ -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 ; if (item.kind === "agent") return ; + if (item.kind === "contextCompaction") return ; return ; } @@ -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 ( + +
{active ? "Compacting context..." : "Context compacted"}
+
+ ); +} + function AgentItem({ item, context }: { item: AgentDisplayItem; context: WorkItemContext }): UiNode { const detailsKey = `${item.id}:agent-details`; const [detailsOpen, setDetailsOpen] = useState(context.openDetails.has(detailsKey)); diff --git a/tests/features/chat/display/display-model.test.ts b/tests/features/chat/display/display-model.test.ts index 4ba902bb..51583e2b 100644 --- a/tests/features/chat/display/display-model.test.ts +++ b/tests/features/chat/display/display-model.test.ts @@ -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", }); diff --git a/tests/features/chat/ui/message-stream/work-log.test.tsx b/tests/features/chat/ui/message-stream/work-log.test.tsx index 53640e28..dc7e9f47 100644 --- a/tests/features/chat/ui/message-stream/work-log.test.tsx +++ b/tests/features/chat/ui/message-stream/work-log.test.tsx @@ -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('[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('[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",