Files
multica/packages/views/common/task-transcript/build-timeline.ts
Antoine GIRARD 9f21d0b634 feat(transcript): add timestamps to run transcript entries (MUL-3174) (#3951)
Threads the existing task_message.created_at column through the full stack (Go protocol -> REST/WS handlers -> TS types -> transcript dialog) so agent run transcripts show per-entry timestamps, helping users spot stalled runs. Additive, no migration.
2026-06-09 19:53:30 +08:00

68 lines
2.1 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;
created_at?: 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 ?? ""}`,
created_at: item.created_at ?? prev.created_at,
};
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,
created_at: msg.created_at,
});
}
return redactTimelineItems(coalesceTimelineItems(items));
}