traces-replay / src /lib /parse.js
mishig's picture
mishig HF Staff
Add Svelte source
741b909 verified
// Normalizes JSONL traces from multiple harnesses (Claude Code, Pi, generic)
// into a uniform Message[] for rendering.
export function parseJsonl(raw) {
const lines = raw.split('\n');
const messages = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
let obj;
try {
obj = JSON.parse(trimmed);
} catch {
continue;
}
const msg = normalize(obj);
if (msg) messages.push(msg);
}
return messages;
}
function normalize(obj) {
// Claude Code format: top-level object has type = 'user' | 'assistant' | 'system'
// and a `message` field.
if (
(obj.type === 'user' || obj.type === 'assistant' || obj.type === 'system') &&
obj.message
) {
return fromClaude(obj);
}
// Pi mono format: `type === 'message'` with message.role inside.
if (obj.type === 'message' && obj.message) {
return fromPi(obj);
}
// Control / meta events.
if (obj.type === 'session') {
return {
role: 'meta',
title: `session${obj.cwd ? ' β€” ' + obj.cwd : ''}`,
blocks: [],
timestamp: obj.timestamp,
};
}
if (obj.type === 'model_change') {
return {
role: 'meta',
title: `model β†’ ${obj.modelId || obj.model || '?'}`,
blocks: [],
timestamp: obj.timestamp,
};
}
if (obj.type === 'thinking_level_change') {
return {
role: 'meta',
title: `thinking level β†’ ${obj.thinkingLevel}`,
blocks: [],
timestamp: obj.timestamp,
};
}
if (obj.type === 'session_info') {
return {
role: 'meta',
title: obj.name || 'session info',
blocks: [],
timestamp: obj.timestamp,
};
}
if (obj.type === 'permission-mode') {
return {
role: 'meta',
title: `permission mode β†’ ${obj.permissionMode}`,
blocks: [],
};
}
// Skip very noisy internal-only records.
if (obj.type === 'file-history-snapshot' || obj.type === 'attachment') {
return null;
}
// Generic fallback: drop as raw meta.
return {
role: 'meta',
title: obj.type || 'event',
blocks: [{ kind: 'raw', json: obj }],
};
}
function fromClaude(obj) {
const role = obj.type; // 'user' | 'assistant' | 'system'
const content = obj.message?.content;
return {
role,
blocks: contentToBlocks(content, 'claude'),
model: obj.message?.model,
timestamp: obj.timestamp,
};
}
function fromPi(obj) {
const innerRole = obj.message?.role; // 'user' | 'assistant' | 'toolResult'
const content = obj.message?.content;
const role = innerRole === 'toolResult' ? 'tool' : innerRole || 'unknown';
return {
role,
blocks: contentToBlocks(content, 'pi', obj.message),
model: obj.message?.model,
timestamp: obj.timestamp,
};
}
function contentToBlocks(content, harness, piMessage) {
const blocks = [];
if (typeof content === 'string') {
blocks.push({ kind: 'text', text: content });
return blocks;
}
if (!Array.isArray(content)) return blocks;
// Pi's toolResult wraps content in an array already; treat the whole thing
// as a tool_result block.
if (harness === 'pi' && piMessage?.role === 'toolResult') {
blocks.push({
kind: 'tool_result',
text: extractText(content),
isError: !!piMessage.isError,
toolCallId: piMessage.toolCallId,
toolName: piMessage.toolName,
});
return blocks;
}
for (const part of content) {
if (!part || typeof part !== 'object') continue;
const t = part.type;
if (t === 'text') {
blocks.push({ kind: 'text', text: part.text ?? '' });
} else if (t === 'thinking') {
blocks.push({ kind: 'thinking', text: part.thinking ?? part.text ?? '' });
} else if (t === 'tool_use' || t === 'toolCall') {
blocks.push({
kind: 'tool_call',
name: part.name || 'tool',
input: part.input ?? part.arguments ?? {},
id: part.id,
});
} else if (t === 'tool_result' || t === 'toolResult') {
blocks.push({
kind: 'tool_result',
text: extractText(part.content),
isError: !!(part.is_error ?? part.isError),
toolCallId: part.tool_use_id ?? part.toolCallId,
});
} else if (t === 'image') {
blocks.push({ kind: 'image', source: part.source });
} else {
blocks.push({ kind: 'raw', json: part });
}
}
return blocks;
}
function extractText(content) {
if (content == null) return '';
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return content
.map((c) => {
if (!c) return '';
if (typeof c === 'string') return c;
if (c.type === 'text') return c.text ?? '';
return JSON.stringify(c, null, 2);
})
.join('\n');
}
try {
return JSON.stringify(content, null, 2);
} catch {
return String(content);
}
}
export function toRawUrl(url) {
// Accept both /blob/ and /resolve/ URLs.
return url.replace('/blob/', '/resolve/');
}