Files
orca/src/main/native-chat/transcript-reader.ts
T
b916248294 Polish desktop native chat view (#6641)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-06-28 15:43:07 -07:00

67 lines
2.6 KiB
TypeScript

import { createReadStream } from 'fs'
import { createInterface } from 'readline'
import type { AgentType, NativeChatMessage } from '../../shared/native-chat-types'
import { errorMessage } from '../ai-vault/session-scanner-values'
import { resolveSessionFilePath, type ResolveSessionFileOptions } from './session-file-resolver'
import { decodeClaudeTranscriptLine, decodeCodexTranscriptLine } from './transcript-line-decoders'
export type ReadTranscriptResult = { messages: NativeChatMessage[] } | { error: string }
export type ReadTranscriptOptions = ResolveSessionFileOptions & {
/** Resolve directly to this file, skipping path discovery (used by tests). */
filePath?: string
}
/**
* Read the ENTIRE Claude/Codex JSONL transcript for an agent + session id into
* the NativeChatMessage model. Unlike the AI-Vault preview scan, this applies
* NO message cap. Unknown record types are skipped rather than throwing, so a
* single malformed/unrecognized line cannot fail the whole read. The per-line
* record→message mapping is shared with the live tailer (transcript-watch.ts)
* via transcript-line-decoders.ts.
*/
export async function readNativeChatTranscript(
agent: AgentType,
sessionId: string,
options: ReadTranscriptOptions = {}
): Promise<ReadTranscriptResult> {
const filePath = options.filePath ?? (await resolveSessionFilePath(agent, sessionId, options))
if (!filePath) {
return { error: `No transcript found for ${agent} session ${sessionId}` }
}
try {
if (agent === 'claude') {
return { messages: await readTranscript(filePath, decodeClaudeTranscriptLine) }
}
if (agent === 'codex') {
return { messages: await readTranscript(filePath, decodeCodexTranscriptLine) }
}
return { error: `Unsupported agent for native chat transcript: ${agent}` }
} catch (err) {
return { error: errorMessage(err) }
}
}
async function readTranscript(
filePath: string,
decode: (line: string, fallbackId: string) => NativeChatMessage | null
): Promise<NativeChatMessage[]> {
const reader = createInterface({
input: createReadStream(filePath, { encoding: 'utf-8' }),
crlfDelay: Infinity
})
const messages: NativeChatMessage[] = []
let index = 0
for await (const line of reader) {
// Why: fallback id embeds start offset 0 so it matches the live tailer's id
// for the same record (the tailer's first drain reads from offset 0 too).
// Records that re-emit then collapse by id in the assembler — no dup, no drop.
const message = decode(line, `${filePath}:0:${index}`)
if (message) {
messages.push(message)
}
index++
}
return messages
}