Simplify completed turn work summary

This commit is contained in:
murashit 2026-06-17 18:19:21 +09:00
parent 14bb1bf4ee
commit a10f18f0ed
6 changed files with 32 additions and 137 deletions

View file

@ -1,21 +1,12 @@
export { messageStreamSemanticClassifications } from "./classify";
export {
messageStreamIsApprovalResult,
messageStreamIsAssistantResponse,
messageStreamIsCommandEvidence,
messageStreamIsContextCompaction,
messageStreamIsCoordinationProgress,
messageStreamIsGoalChange,
messageStreamIsHookEvidence,
messageStreamIsPermissionDecision,
messageStreamIsProposedPlan,
messageStreamIsReasoningProgress,
messageStreamIsReviewResult,
messageStreamIsTaskProgress,
messageStreamIsToolEvidence,
messageStreamIsTurnInitiator,
messageStreamIsTurnSteer,
messageStreamIsUserInputResult,
messageStreamIsWorkspaceResult,
messageStreamRenderFamily,
} from "./predicates";

View file

@ -27,14 +27,6 @@ export function messageStreamIsTurnSteer(classification: MessageStreamSemanticCl
);
}
export function messageStreamIsProposedPlan(classification: MessageStreamSemanticClassification): boolean {
return messageStreamHasMeaning(classification, "dialogue", "proposal");
}
export function messageStreamIsAssistantResponse(classification: MessageStreamSemanticClassification): boolean {
return messageStreamHasMeaning(classification, "dialogue", "response");
}
export function messageStreamIsWorkspaceResult(classification: MessageStreamSemanticClassification): boolean {
return messageStreamHasMeaning(classification, "workspace", "result");
}
@ -51,60 +43,29 @@ export function messageStreamIsReasoningProgress(classification: MessageStreamSe
return classification.item.kind === "reasoning";
}
export function messageStreamIsContextCompaction(classification: MessageStreamSemanticClassification): boolean {
return classification.item.kind === "contextCompaction";
}
export function messageStreamIsCommandEvidence(classification: MessageStreamSemanticClassification): boolean {
return classification.item.kind === "command";
}
export function messageStreamIsToolEvidence(classification: MessageStreamSemanticClassification): boolean {
return classification.item.kind === "tool";
}
export function messageStreamIsHookEvidence(classification: MessageStreamSemanticClassification): boolean {
return classification.item.kind === "hook";
}
export function messageStreamIsApprovalResult(classification: MessageStreamSemanticClassification): boolean {
return classification.item.kind === "approvalResult";
}
export function messageStreamIsUserInputResult(classification: MessageStreamSemanticClassification): boolean {
return classification.item.kind === "userInputResult";
}
export function messageStreamIsReviewResult(classification: MessageStreamSemanticClassification): boolean {
return classification.item.kind === "reviewResult";
}
export function messageStreamIsGoalChange(classification: MessageStreamSemanticClassification): boolean {
return classification.item.kind === "goal";
}
export function messageStreamRenderFamily(classification: MessageStreamSemanticClassification): MessageStreamRenderFamily | null {
if (classification.item.kind === "message" || classification.item.kind === "system" || messageStreamIsUserInputResult(classification)) {
return "text";
}
if (
messageStreamIsCommandEvidence(classification) ||
messageStreamIsWorkspaceResult(classification) ||
messageStreamIsToolEvidence(classification) ||
messageStreamIsHookEvidence(classification) ||
messageStreamIsGoalChange(classification) ||
messageStreamIsApprovalResult(classification) ||
messageStreamIsReviewResult(classification)
) {
return "toolResult";
}
if (
messageStreamIsTaskProgress(classification) ||
messageStreamIsCoordinationProgress(classification) ||
messageStreamIsReasoningProgress(classification) ||
messageStreamIsContextCompaction(classification)
) {
return "work";
switch (classification.item.kind) {
case "message":
case "system":
case "userInputResult":
return "text";
case "command":
case "fileChange":
case "tool":
case "hook":
case "goal":
case "approvalResult":
case "reviewResult":
return "toolResult";
case "taskProgress":
case "agent":
case "reasoning":
case "contextCompaction":
return "work";
}
return null;
}

View file

@ -1,22 +1,11 @@
import type { MessageStreamItem } from "../../domain/message-stream/items";
import { pathRelativeToRoot } from "../../domain/message-stream/format/path-labels";
import {
messageStreamIsApprovalResult,
messageStreamIsAssistantResponse,
messageStreamIsCommandEvidence,
messageStreamIsContextCompaction,
messageStreamIsCoordinationProgress,
messageStreamIsGoalChange,
messageStreamIsHookEvidence,
messageStreamIsPermissionDecision,
messageStreamIsProposedPlan,
messageStreamIsReasoningProgress,
messageStreamIsReviewResult,
messageStreamIsTaskProgress,
messageStreamIsToolEvidence,
messageStreamIsTurnInitiator,
messageStreamIsTurnSteer,
messageStreamIsUserInputResult,
messageStreamIsWorkspaceResult,
messageStreamSemanticClassifications,
} from "../../domain/message-stream/semantics";
@ -103,7 +92,7 @@ export function messageStreamLayoutBlocks(
type: "activityGroup",
id: `turn-${turnId}-activity`,
turnId,
summary: turnActivitySummary(groupItems),
summary: "Work details",
items: groupItems,
});
}
@ -218,55 +207,6 @@ function autoReviewSummariesForTurns(items: readonly MessageStreamSemanticClassi
return byTurn;
}
function turnActivitySummary(items: readonly GroupedActivity[]): string {
const parts = [
countActivityLabel(
items,
(item) => item.type === "item" && messageStreamIsAssistantResponse(item.classification),
"response",
"responses",
),
countActivityLabel(items, (item) => item.type === "item" && messageStreamIsProposedPlan(item.classification), "plan", "plans"),
countActivityLabel(items, (item) => item.type === "steering", "steer", "steers"),
countActivityLabel(items, (item) => item.type === "item" && messageStreamIsTaskProgress(item.classification), "task progress"),
countActivityLabel(items, (item) => item.type === "item" && messageStreamIsCoordinationProgress(item.classification), "agent"),
countActivityLabel(items, (item) => item.type === "item" && messageStreamIsCommandEvidence(item.classification), "command"),
countActivityLabel(items, (item) => item.type === "item" && messageStreamIsWorkspaceResult(item.classification), "file change"),
countActivityLabel(items, (item) => item.type === "item" && messageStreamIsToolEvidence(item.classification), "tool"),
countActivityLabel(items, (item) => item.type === "item" && messageStreamIsHookEvidence(item.classification), "hook"),
countActivityLabel(
items,
(item) => item.type === "item" && messageStreamIsReasoningProgress(item.classification),
"thought",
"thought notes",
),
countActivityLabel(
items,
(item) => item.type === "item" && messageStreamIsContextCompaction(item.classification),
"context compaction",
),
countActivityLabel(items, (item) => item.type === "item" && messageStreamIsApprovalResult(item.classification), "approval"),
countActivityLabel(items, (item) => item.type === "item" && messageStreamIsUserInputResult(item.classification), "input"),
countActivityLabel(items, (item) => item.type === "item" && messageStreamIsReviewResult(item.classification), "review"),
countActivityLabel(items, (item) => item.type === "item" && messageStreamIsGoalChange(item.classification), "goal"),
].filter((part): part is string => Boolean(part));
if (parts.length === 0) return "Work details";
return `Work details: ${parts.join(", ")}`;
}
function countActivityLabel(
items: readonly GroupedActivity[],
predicate: (item: GroupedActivity) => boolean,
label: string,
pluralLabel = `${label}s`,
): string | null {
const count = items.filter(predicate).length;
if (count === 0) return null;
if (count === 1) return label;
return `${String(count)} ${pluralLabel}`;
}
function textForMessageStreamItem(item: MessageStreamItem): string {
return "text" in item && typeof item.text === "string" ? item.text : "";
}

View file

@ -916,7 +916,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes
const blocks = messageStreamLayoutBlocks(items, null);
expect(blocks.map((block) => block.type)).toEqual(["item", "activityGroup", "item"]);
expect(blocks[1]).toMatchObject({ summary: "Work details: command, thought" });
expect(blocks[1]).toMatchObject({ summary: "Work details" });
});
it("groups completed hook and review logs before the final assistant message", () => {
@ -950,7 +950,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "turn-t1-activity", "a1"]);
expect(blocks[1]).toMatchObject({
summary: "Work details: command, hook, thought, 2 reviews",
summary: "Work details",
items: [
{ item: { id: "hook-1" } },
{ item: { id: "c1" } },
@ -1021,7 +1021,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes
const blocks = messageStreamLayoutBlocks(items, null);
expect(blocks.map((block) => block.type)).toEqual(["item", "activityGroup", "item"]);
expect(blocks[1]).toMatchObject({
summary: "Work details: 2 responses, command, file change",
summary: "Work details",
items: [{ item: { id: "a1" } }, { item: { id: "c1" } }, { item: { id: "a2" } }, { item: { id: "f1" } }],
});
expect(blocks[2]).toMatchObject({ item: { id: "a3" } });
@ -1069,7 +1069,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "u2", "turn-t1-activity", "a1"]);
expect(blocks[2]).toMatchObject({
summary: "Work details: steer, 2 commands",
summary: "Work details",
items: [
{ type: "item", item: { id: "c1" } },
{
@ -1084,7 +1084,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes
});
});
it("pluralizes multiple steering markers as steers in completed turn work details", () => {
it("keeps multiple steering markers in completed turn work details", () => {
const items: MessageStreamItem[] = [
{ id: "u1", kind: "message", messageKind: "user", role: "user", text: "do it", turnId: "t1" },
{ id: "u2", kind: "message", messageKind: "user", role: "user", text: "also check tests", turnId: "t1" },
@ -1104,7 +1104,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes
const activityGroup = blocks.find((block) => block.type === "activityGroup");
expect(activityGroup).toMatchObject({
summary: "Work details: 2 steers",
summary: "Work details",
items: [
{ type: "steering", id: "steer-activity-u2" },
{ type: "steering", id: "steer-activity-u3" },
@ -1158,7 +1158,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "plan-progress-t1", "a1"]);
});
it("summarizes task progress and agent activity separately from tools", () => {
it("groups task progress and agent activity inside completed turn work details", () => {
const items: MessageStreamItem[] = [
{ id: "u1", kind: "message", messageKind: "user", role: "user", text: "do it", turnId: "t1" },
{
@ -1198,7 +1198,10 @@ describe("display block grouping keeps work logs subordinate to conversation mes
];
const blocks = messageStreamLayoutBlocks(items, null);
expect(blocks[1]).toMatchObject({ summary: "Work details: task progress, agent" });
expect(blocks[1]).toMatchObject({
summary: "Work details",
items: [{ item: { id: "plan-progress-t1" } }, { item: { id: "agent-1" } }],
});
});
it("summarizes active subagent states while a turn is running", () => {

View file

@ -90,7 +90,7 @@ describe("message stream rendering and message actions", () => {
),
).toEqual(["item:u1", "activity:turn-t1-activity", "item:a1"]);
const activitySummary = parent.querySelector<HTMLElement>('[data-codex-panel-block-key="activity:turn-t1-activity"] summary');
expect(activitySummary?.textContent).toBe("Work details: hook");
expect(activitySummary?.textContent).toBe("Work details");
expect(activitySummary?.tabIndex).toBe(-1);
unmountUiRootInAct(parent);
});

View file

@ -330,7 +330,7 @@ describe("work log renderer decisions", () => {
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(":scope > summary")?.textContent).toBe("Work details");
expect(element.querySelector(".codex-panel__tool-item--hook")?.classList.contains("codex-panel__execution--completed")).toBe(true);
expect(element.querySelector(".codex-panel__tool-summary")?.textContent).toBe("postToolUse: Formatted 1 file.");
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("statuscompleted");