diff --git a/src/features/chat/app-server/mappers/message-stream/turn-items.ts b/src/features/chat/app-server/mappers/message-stream/turn-items.ts index f0cedcf0..7dd7faeb 100644 --- a/src/features/chat/app-server/mappers/message-stream/turn-items.ts +++ b/src/features/chat/app-server/mappers/message-stream/turn-items.ts @@ -273,11 +273,9 @@ function imageViewMessageStreamItem(item: ImageViewItem, turnId?: string): Messa function sleepMessageStreamItem(item: SleepItem, turnId?: string): MessageStreamItem { return { ...turnItemSourceFields(item, turnId), - kind: "tool", + kind: "wait", role: "tool", - toolName: "sleep", - primaryTarget: { kind: "value", value: `${String(item.durationMs)} ms` }, - output: "", + text: `Waited ${durationLabel(item.durationMs)}`, executionState: "completed", }; } @@ -569,3 +567,9 @@ function jsonTargetPrimitive(value: unknown): string | null { function executionStateFromStatus(status: string, states: ExecutionStateByStatus): ExecutionState { return states[status] ?? null; } + +function durationLabel(durationMs: number): string { + if (durationMs < 1000) return `${String(durationMs)}ms`; + if (durationMs % 1000 === 0) return `${String(durationMs / 1000)}s`; + return `${String(durationMs / 1000)}s`; +} diff --git a/src/features/chat/domain/message-stream/items.ts b/src/features/chat/domain/message-stream/items.ts index a1e6006e..eb913dd1 100644 --- a/src/features/chat/domain/message-stream/items.ts +++ b/src/features/chat/domain/message-stream/items.ts @@ -10,6 +10,7 @@ export type MessageStreamItemKind = | "agent" | "hook" | "reasoning" + | "wait" | "contextCompaction" | "system" | "goal" @@ -246,6 +247,12 @@ interface ContextCompactionMessageStreamItem extends MessageStreamBase { readonly role: "tool"; } +interface WaitMessageStreamItem extends MessageStreamBase { + readonly kind: "wait"; + readonly role: "tool"; + readonly text: string; +} + interface TaskProgressStep { readonly step: string; readonly status: "pending" | "inProgress" | "completed"; @@ -304,6 +311,7 @@ export type MessageStreamItem = | ToolCallMessageStreamItem | HookMessageStreamItem | ReasoningMessageStreamItem + | WaitMessageStreamItem | ContextCompactionMessageStreamItem | TaskProgressMessageStreamItem | AgentMessageStreamItem diff --git a/src/features/chat/domain/message-stream/semantics/classify.ts b/src/features/chat/domain/message-stream/semantics/classify.ts index 7e430076..517f8f3d 100644 --- a/src/features/chat/domain/message-stream/semantics/classify.ts +++ b/src/features/chat/domain/message-stream/semantics/classify.ts @@ -85,6 +85,8 @@ function meaningForMessageStreamItem(item: MessageStreamItem): MessageStreamMean return { plane: "execution", event: "evidence" }; case "reasoning": return { plane: "execution", event: "progress" }; + case "wait": + return { plane: "execution", event: "progress" }; case "taskProgress": return { plane: "execution", event: "progress" }; case "agent": diff --git a/src/features/chat/presentation/message-stream/render-family.ts b/src/features/chat/presentation/message-stream/render-family.ts index 6769e649..0c2c0de1 100644 --- a/src/features/chat/presentation/message-stream/render-family.ts +++ b/src/features/chat/presentation/message-stream/render-family.ts @@ -19,6 +19,7 @@ export function messageStreamRenderFamily(classification: MessageStreamSemanticC return "detail"; case "taskProgress": case "reasoning": + case "wait": case "contextCompaction": return "status"; } diff --git a/tests/features/chat/message-stream/model.test.ts b/tests/features/chat/message-stream/model.test.ts index 08e4353a..a6065281 100644 --- a/tests/features/chat/message-stream/model.test.ts +++ b/tests/features/chat/message-stream/model.test.ts @@ -683,7 +683,7 @@ describe("turn item conversion preserves app-server semantics", () => { }); }); - it("preserves sleep items as completed wait tool summaries", () => { + it("preserves sleep items as completed wait status summaries", () => { const item: TurnItem = { type: "sleep", id: "sleep-1", @@ -691,10 +691,8 @@ describe("turn item conversion preserves app-server semantics", () => { }; expect(messageStreamItemFromTurnItem(item, "t1")).toMatchObject({ - kind: "tool", - toolName: "sleep", - primaryTarget: { kind: "value", value: "2500 ms" }, - output: "", + kind: "wait", + text: "Waited 2.5s", executionState: "completed", }); }); diff --git a/tests/features/chat/message-stream/semantics.test.ts b/tests/features/chat/message-stream/semantics.test.ts index ac41e29e..1ae64a48 100644 --- a/tests/features/chat/message-stream/semantics.test.ts +++ b/tests/features/chat/message-stream/semantics.test.ts @@ -79,6 +79,7 @@ describe("message stream semantic classification", () => { { id: "tool", kind: "tool", role: "tool", text: "tool", toolCall: { arguments: { k: "v" } } }, { id: "hook", kind: "hook", role: "tool", text: "hook" }, { id: "reasoning", kind: "reasoning", role: "tool", text: "thinking" }, + { id: "wait", kind: "wait", role: "tool", text: "Waited 2.5s", executionState: "completed" }, ]); expect(semantic.map(({ meaning }) => meaning)).toEqual([ @@ -87,6 +88,7 @@ describe("message stream semantic classification", () => { { plane: "execution", event: "evidence" }, { plane: "execution", event: "evidence" }, { plane: "execution", event: "progress" }, + { plane: "execution", event: "progress" }, ]); });