| |
| |
|
|
| 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) { |
| |
| |
| if ( |
| (obj.type === 'user' || obj.type === 'assistant' || obj.type === 'system') && |
| obj.message |
| ) { |
| return fromClaude(obj); |
| } |
|
|
| |
| if (obj.type === 'message' && obj.message) { |
| return fromPi(obj); |
| } |
|
|
| |
| 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: [], |
| }; |
| } |
| |
| if (obj.type === 'file-history-snapshot' || obj.type === 'attachment') { |
| return null; |
| } |
|
|
| |
| return { |
| role: 'meta', |
| title: obj.type || 'event', |
| blocks: [{ kind: 'raw', json: obj }], |
| }; |
| } |
|
|
| function fromClaude(obj) { |
| const role = obj.type; |
| 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; |
| 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; |
|
|
| |
| |
| 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) { |
| |
| return url.replace('/blob/', '/resolve/'); |
| } |
|
|