Spaces:
Running
Running
| // 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/'); | |
| } | |