diff --git a/src/features/chat/domain/message-stream/semantics/index.ts b/src/features/chat/domain/message-stream/semantics/index.ts index cd884ccd..f14097cd 100644 --- a/src/features/chat/domain/message-stream/semantics/index.ts +++ b/src/features/chat/domain/message-stream/semantics/index.ts @@ -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"; diff --git a/src/features/chat/domain/message-stream/semantics/predicates.ts b/src/features/chat/domain/message-stream/semantics/predicates.ts index 5f9d90f2..c25057ce 100644 --- a/src/features/chat/domain/message-stream/semantics/predicates.ts +++ b/src/features/chat/domain/message-stream/semantics/predicates.ts @@ -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; } diff --git a/src/features/chat/presentation/message-stream/layout.ts b/src/features/chat/presentation/message-stream/layout.ts index 75ac04c1..fc57792e 100644 --- a/src/features/chat/presentation/message-stream/layout.ts +++ b/src/features/chat/presentation/message-stream/layout.ts @@ -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 : ""; } diff --git a/tests/features/chat/message-stream/model.test.ts b/tests/features/chat/message-stream/model.test.ts index 7206b77c..c1920236 100644 --- a/tests/features/chat/message-stream/model.test.ts +++ b/tests/features/chat/message-stream/model.test.ts @@ -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", () => { diff --git a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx index 4f33178f..5aba003f 100644 --- a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx +++ b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx @@ -90,7 +90,7 @@ describe("message stream rendering and message actions", () => { ), ).toEqual(["item:u1", "activity:turn-t1-activity", "item:a1"]); const activitySummary = parent.querySelector('[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); }); 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 95cfb5be..5d699a0a 100644 --- a/tests/features/chat/ui/message-stream/work-log.test.tsx +++ b/tests/features/chat/ui/message-stream/work-log.test.tsx @@ -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");