diff --git a/src/main/services/team/TaskChangeLedgerReader.ts b/src/main/services/team/TaskChangeLedgerReader.ts index b9b7ec49..00d301bd 100644 --- a/src/main/services/team/TaskChangeLedgerReader.ts +++ b/src/main/services/team/TaskChangeLedgerReader.ts @@ -943,8 +943,9 @@ export class TaskChangeLedgerReader { private mapV2SummaryFile(file: LedgerSummaryFileV2, projectPath?: string): FileChangeSummary { const displayPath = file.displayPath ?? file.filePath; + const filePath = this.normalizeLedgerFilePath(file.filePath); return { - filePath: file.filePath, + filePath, relativePath: this.relativePath(displayPath, projectPath, file.relativePath), snippets: [], linesAdded: file.linesAdded, @@ -1001,7 +1002,7 @@ export class TaskChangeLedgerReader { startTimestamp: scope.startTimestamp, endTimestamp: scope.endTimestamp, toolUseIds: scope.toolUseIds, - filePaths: files.map((file) => file.filePath), + filePaths: files.map((file) => this.normalizeLedgerFilePath(file.filePath)), confidence: scope.confidence, ...(scope.primaryActorKey ? { primaryActorKey: scope.primaryActorKey } : {}), ...(scope.primaryAgentId ? { primaryAgentId: scope.primaryAgentId } : {}), @@ -1049,9 +1050,10 @@ export class TaskChangeLedgerReader { beforeContent: string | null, afterContent: string | null ): SnippetDiff { + const filePath = this.normalizeLedgerFilePath(event.filePath); return { toolUseId: event.toolUseId, - filePath: event.filePath, + filePath, toolName: this.mapToolName(event.source), type: this.mapSnippetType(event), oldString: event.oldString ?? beforeContent ?? '', @@ -1371,7 +1373,18 @@ export class TaskChangeLedgerReader { return null; } - return `${normalizedAnchor.slice(0, normalizedAnchor.length - normalizedAnchorRelation.length)}${targetRelationPath.replace(/\\/g, '/')}`; + return this.normalizeLedgerFilePath( + `${normalizedAnchor.slice(0, normalizedAnchor.length - normalizedAnchorRelation.length)}${targetRelationPath.replace(/\\/g, '/')}` + ); + } + + private normalizeLedgerFilePath(filePath: string): string { + const slashPath = filePath.replace(/\\/g, '/'); + const isWindowsAbsolute = /^[A-Za-z]:\//.test(slashPath) || slashPath.startsWith('//'); + if (path.isAbsolute(filePath) || isWindowsAbsolute) { + return path.normalize(filePath); + } + return slashPath; } private relativePath( diff --git a/test/main/services/team/taskChangeLedgerFixtureUtils.ts b/test/main/services/team/taskChangeLedgerFixtureUtils.ts index ed87c88e..02e88f63 100644 --- a/test/main/services/team/taskChangeLedgerFixtureUtils.ts +++ b/test/main/services/team/taskChangeLedgerFixtureUtils.ts @@ -61,6 +61,41 @@ async function rewriteProjectRootTokens(rootDir: string, token: string, projectD } } +function shouldNormalizeLfFixtureFile(filePath: string): boolean { + const normalizedPath = filePath.replace(/\\/g, '/'); + return ( + /\.(json|jsonl|md|txt|ts|tsx|js|jsx)$/.test(normalizedPath) || + normalizedPath.includes('/.board-task-changes/blobs/sha256/') + ); +} + +function looksBinary(buffer: Buffer): boolean { + for (const byte of buffer) { + if (byte === 0) return true; + if (byte < 9 || (byte > 13 && byte < 32)) return true; + } + return false; +} + +async function normalizeFixtureTextLineEndings(rootDir: string): Promise { + const entries = await fs.readdir(rootDir, { withFileTypes: true }); + for (const entry of entries) { + const entryPath = path.join(rootDir, entry.name); + if (entry.isDirectory()) { + await normalizeFixtureTextLineEndings(entryPath); + continue; + } + if (!shouldNormalizeLfFixtureFile(entryPath)) { + continue; + } + const raw = await fs.readFile(entryPath); + if (!raw.includes(13) || looksBinary(raw)) { + continue; + } + await fs.writeFile(entryPath, raw.toString('utf8').replace(/\r\n?/g, '\n'), 'utf8'); + } +} + export async function materializeTaskChangeLedgerFixture( fixtureName: string ): Promise { @@ -76,6 +111,7 @@ export async function materializeTaskChangeLedgerFixture( const token = manifest.projectRootToken ?? DEFAULT_PROJECT_ROOT_TOKEN; await rewriteProjectRootTokens(rootDir, token, projectDir); + await normalizeFixtureTextLineEndings(rootDir); return { rootDir,