Files
multica/packages/views/common/task-transcript/build-timeline.ts
2026-05-28 12:34:51 +08:00

65 lines
2.0 KiB
TypeScript

import type { TaskMessagePayload } from "@multica/core/types/events";
import { redactSecrets } from "./redact";
/** A unified timeline entry: tool calls, thinking, text, and errors in chronological order. */
export interface TimelineItem {
seq: number;
type: "tool_use" | "tool_result" | "thinking" | "text" | "error";
tool?: string;
content?: string;
input?: Record<string, unknown>;
output?: string;
}
function canMergeStreamingText(prev: TimelineItem, next: TimelineItem): boolean {
return (prev.type === "thinking" || prev.type === "text") && prev.type === next.type;
}
/** Merge adjacent text/thinking fragments that were split only by daemon flush timing. */
export function coalesceTimelineItems(items: TimelineItem[]): TimelineItem[] {
const sorted = [...items].sort((a, b) => a.seq - b.seq);
const out: TimelineItem[] = [];
for (const item of sorted) {
const prev = out[out.length - 1];
if (prev && canMergeStreamingText(prev, item)) {
out[out.length - 1] = {
...prev,
content: `${prev.content ?? ""}${item.content ?? ""}`,
};
continue;
}
out.push(item);
}
return out;
}
export function appendTimelineItem(items: TimelineItem[], item: TimelineItem): TimelineItem[] {
return coalesceTimelineItems([...items, item]);
}
function redactTimelineItems(items: TimelineItem[]): TimelineItem[] {
return items.map((item) => ({
...item,
content: item.content ? redactSecrets(item.content) : item.content,
output: item.output ? redactSecrets(item.output) : item.output,
}));
}
/** Build a chronologically ordered timeline from raw task messages. */
export function buildTimeline(msgs: TaskMessagePayload[]): TimelineItem[] {
const items: TimelineItem[] = [];
for (const msg of msgs) {
items.push({
seq: msg.seq,
type: msg.type,
tool: msg.tool,
content: msg.content,
input: msg.input,
output: msg.output,
});
}
return redactTimelineItems(coalesceTimelineItems(items));
}