<<<CODE_BUNDLE bundle_id="8e440957a735360b" expected_change_file="code-change-8e440957a735360b.txt">>>

<<<AI_TASK_INSTRUCTIONS priority="highest">>>
This attachment is a complete source-code bundle, not a summary or excerpt.
Before answering, use Python or equivalent file-processing tools to open and read the attachment itself from beginning to end. Do not rely only on an attachment preview, search snippet, extracted summary, or truncated chat context.
Verify that each BEGIN_FILE marker has a matching END_FILE marker. If the complete attachment cannot be accessed, explicitly state that limitation instead of treating a visible excerpt as the whole file.
For analysis requests, inspect the complete contents of all relevant file blocks and explain the project structure, relationships, and implementation details.
For code-editing requests, follow the AI Editing Response Protocol below and return a downloadable plain-text CHANGE file named code-change-8e440957a735360b.txt containing only complete changed-file blocks.
Do not output partial snippets as the final edit result. Do not invent content that was not read from the attachment.
<<<END_AI_TASK_INSTRUCTIONS>>>

# CODE BUNDLE FOR AI ANALYSIS

## Bundle Identity

- Bundle ID: 8e440957a735360b
- Required change file name: code-change-8e440957a735360b.txt

## 1. Reading Instructions

This document is a complete-file source bundle generated from user-selected files and folders.

AI analysis instruction: first open and read the complete attached file with Python or equivalent file-processing tools. Do not rely on a preview or truncated extraction. Then inspect every relevant BEGIN_FILE/END_FILE block, extract structure and relationships, and provide a detailed answer.

Important reading rules:

- Every included file is represented as a complete file.
- No file content has been split into functions, classes, snippets, or summaries.
- Each file starts with a BEGIN_FILE marker and ends with an END_FILE marker.
- Each BEGIN_FILE marker includes a SHA-256 hash of the exact decoded text content.
- The `path` attribute is the workspace-relative file path.
- The `language` attribute is inferred from the file name or extension.
- The `size` attribute is the original file size before bundling.
- When analyzing this bundle, treat each file block as the authoritative full content of that file.
- Some files and directories may have been skipped. See the final section, "Skipped Items".

Recommended analysis order for AI:

1. Read the Bundle Overview.
2. Read the Packed File Index.
3. Read the Project File Tree.
4. Then read the Complete File Contents in order.
5. Use the Skipped Items section only to understand what was intentionally excluded.

---

## 2. Bundle Overview

- Workspace Root Name: code-bundler
- Generated At: 2026-08-09T08:25:49.654Z
- Total Scanned Files: 28
- Packed Complete Files: 28
- Skipped Files: 0
- Skipped Directories: 0
- Packed Source Size: 186.2 KB
- Bundle Format Version: 2.0
- File Content Mode: Complete files only

---

## 3. Bundle Scope

This bundle contains the complete text content of files that passed the bundling filters.

Included files are selected according to these rules:

- Regular files are scanned recursively from the selected files and folders.
- Files that look like text are included.
- Binary files are excluded.
- Ignored folders are excluded.
- Ignored files are excluded.
- Oversized files are excluded.
- The generated bundle file itself is excluded.

This bundle does not contain:

- Dependency folders such as `node_modules`.
- Git metadata such as `.git`.
- Build output folders such as `dist`, `out`, or `build`.
- Binary assets such as images, videos, archives, PDFs, or Office documents.
- Files that could not be safely read as text.

---

## 4. Packed File Index

The following files are included as complete files.

```text
src/extension.ts | typescript | 7.6 KB
src/bundle/builder.ts | typescript | 16.1 KB
src/bundle/bundleQueueManager.ts | typescript | 7.1 KB
src/bundle/collector.ts | typescript | 6.9 KB
src/bundle/fileTree.ts | typescript | 3.1 KB
src/bundle/language.ts | typescript | 2.9 KB
src/changes/appliedReviewManager.ts | typescript | 29.5 KB
src/changes/appliedSession.ts | typescript | 6.1 KB
src/changes/changeFileOpenPrompt.ts | typescript | 2.6 KB
src/changes/decorations.ts | typescript | 27.4 KB
src/changes/diffHunks.ts | typescript | 24.7 KB
src/changes/parser.ts | typescript | 3.8 KB
src/changes/reviewCodeLens.ts | typescript | 7.1 KB
src/changes/reviewContentProvider.ts | typescript | 1012 B
src/changes/reviewContext.ts | typescript | 310 B
src/changes/reviewedLineMap.ts | typescript | 1.2 KB
src/changes/reviewFileListView.ts | typescript | 4.9 KB
src/changes/validator.ts | typescript | 2.8 KB
src/commands/applyChangeBundleCommand.ts | typescript | 5.7 KB
src/commands/packCommand.ts | typescript | 13.1 KB
src/constants.ts | typescript | 2.0 KB
src/test/extension.test.ts | typescript | 474 B
src/types.ts | typescript | 1.1 KB
src/utils/clipboard.ts | typescript | 2.3 KB
src/utils/encoding.ts | typescript | 4.0 KB
src/utils/fileSystem.ts | typescript | 2.0 KB
src/utils/format.ts | typescript | 265 B
src/utils/hash.ts | typescript | 188 B
```

Index line format:

```text
relative/path/to/file.ext | language | size
```

---

## 5. Project File Tree

The following tree only shows files included in this bundle.

```text
.
└── src/
    ├── bundle/
    │   ├── builder.ts
    │   ├── bundleQueueManager.ts
    │   ├── collector.ts
    │   ├── fileTree.ts
    │   └── language.ts
    ├── changes/
    │   ├── appliedReviewManager.ts
    │   ├── appliedSession.ts
    │   ├── changeFileOpenPrompt.ts
    │   ├── decorations.ts
    │   ├── diffHunks.ts
    │   ├── parser.ts
    │   ├── reviewCodeLens.ts
    │   ├── reviewContentProvider.ts
    │   ├── reviewContext.ts
    │   ├── reviewedLineMap.ts
    │   ├── reviewFileListView.ts
    │   └── validator.ts
    ├── commands/
    │   ├── applyChangeBundleCommand.ts
    │   └── packCommand.ts
    ├── test/
    │   └── extension.test.ts
    ├── utils/
    │   ├── clipboard.ts
    │   ├── encoding.ts
    │   ├── fileSystem.ts
    │   ├── format.ts
    │   └── hash.ts
    ├── constants.ts
    ├── extension.ts
    └── types.ts
```

---

## 6. Complete File Contents

All files below are included in full.

Each file uses this structure:

```text
<<<BEGIN_FILE path="src/example.ts" language="typescript" size="2.1 KB" sha256="...">>>
complete file content
<<<END_FILE path="src/example.ts">>>
```

Do not treat the markers as part of the original file content.

### 6.1 AI Editing Response Protocol

This protocol is mandatory for every code-editing response based on this attachment.

1. Read the complete matching BEGIN_FILE block before editing it.
2. Return the final result as a downloadable plain-text CHANGE file using the `code-change-<bundle-id>.txt` name specified in the bundle, not pasted fragments or prose only.
3. Return only changed files using CHANGE blocks.
4. Use operation="modify", "create", "delete", or "rename".
5. For modify, delete, or rename, copy original_sha256 from the matching BEGIN_FILE marker.
6. For rename, include new_path and do not include file content.
7. For modify or create, put the complete resulting file content inside the block.
8. Do not copy protocol examples or unchanged files into the response.

### 6.2 Save the AI Response

1. Save the returned CHANGE content using the `code-change-<bundle-id>.txt` name specified in the bundle.
2. Do not remove or manually alter these markers:

```text
<<<BEGIN_CHANGE operation="modify" path="src/example.ts" original_sha256="<copy-from-BEGIN_FILE>">>>
complete resulting file content
<<<END_CHANGE path="src/example.ts">>>
```

---

<<<BEGIN_FILE path="src/extension.ts" language="typescript" size="7.6 KB" sha256="b2cf665eea8e74cb27413cbf96b11a005ad7c6325982729e70c1a42f2b66493a">>>
import * as vscode from 'vscode';

import {
    registerPackCommand
} from './commands/packCommand';

import {
    registerApplyChangeBundleCommand
} from './commands/applyChangeBundleCommand';

import {
    BundleQueueManager
} from './bundle/bundleQueueManager';

import {
    ChangeFileOpenPrompt
} from './changes/changeFileOpenPrompt';

import {
    AppliedReviewManager
} from './changes/appliedReviewManager';

import {
    PendingChangeDecorations
} from './changes/decorations';

import {
    ReviewCodeLensProvider
} from './changes/reviewCodeLens';

import {
    ReviewContentProvider
} from './changes/reviewContentProvider';

import {
    ReviewFileListView
} from './changes/reviewFileListView';

export function activate(
    context: vscode.ExtensionContext
): void {
    const outputChannel =
        vscode.window.createOutputChannel(
            'Code Bundler'
        );


    const bundleQueueManager =
        new BundleQueueManager(context);


    const changeFileOpenPrompt =
        new ChangeFileOpenPrompt();

    const decorations =
        new PendingChangeDecorations();

    const reviewContentProvider =
        new ReviewContentProvider();

    const reviewContentRegistration =
        vscode.workspace
            .registerTextDocumentContentProvider(
                ReviewContentProvider.scheme,
                reviewContentProvider
            );

    const reviewManager =
        new AppliedReviewManager(
            decorations,
            outputChannel,
            reviewContentProvider
        );

    const reviewFileListView =
        new ReviewFileListView(
            decorations
        );

    const reviewFileListRegistration =
        vscode.window.registerWebviewViewProvider(
            ReviewFileListView.viewType,
            reviewFileListView
        );

    const codeLensProvider =
        new ReviewCodeLensProvider(
            decorations
        );

    const codeLensRegistration =
        vscode.languages
            .registerCodeLensProvider(
                {
                    scheme: 'file'
                },
                codeLensProvider
            );

    const fileDecorationRegistration =
        vscode.window
            .registerFileDecorationProvider(
                decorations
            );

    const previousChangeCommand =
        vscode.commands.registerCommand(
            'code-bundler.previousChange',
            async () => {
                await runReviewCommand(
                    () =>
                        reviewManager.previousChange()
                );
            }
        );

    const nextChangeCommand =
        vscode.commands.registerCommand(
            'code-bundler.nextChange',
            async () => {
                await runReviewCommand(
                    () =>
                        reviewManager.nextChange()
                );
            }
        );

    const previousPendingFileCommand =
        vscode.commands.registerCommand(
            'code-bundler.previousPendingFile',
            async () => {
                await runReviewCommand(
                    () => reviewManager
                        .previousPendingFile()
                );
            }
        );

    const nextPendingFileCommand =
        vscode.commands.registerCommand(
            'code-bundler.nextPendingFile',
            async () => {
                await runReviewCommand(
                    () => reviewManager
                        .nextPendingFile()
                );
            }
        );

    const openDiffCommand =
        vscode.commands.registerCommand(
            'code-bundler.openCurrentDiff',
            async (uri?: vscode.Uri) => {
                await runReviewCommand(
                    () => reviewManager
                        .openCurrentDiff(uri)
                );
            }
        );

    const acceptCurrentCommand =
        vscode.commands.registerCommand(
            'code-bundler.acceptCurrentChange',
            async (uri?: vscode.Uri) => {
                await runReviewCommand(
                    () =>
                        reviewManager
                            .acceptCurrent(uri)
                );
            }
        );

    const rejectCurrentCommand =
        vscode.commands.registerCommand(
            'code-bundler.rejectCurrentChange',
            async (uri?: vscode.Uri) => {
                await runReviewCommand(
                    () =>
                        reviewManager
                            .rejectCurrent(uri)
                );
            }
        );

    const acceptHunkCommand =
        vscode.commands.registerCommand(
            'code-bundler.acceptHunk',
            async (
                uri: vscode.Uri,
                hunkId: string
            ) => {
                await runReviewCommand(
                    () =>
                        reviewManager.acceptHunk(
                            uri,
                            hunkId
                        )
                );
            }
        );

    const rejectHunkCommand =
        vscode.commands.registerCommand(
            'code-bundler.rejectHunk',
            async (
                uri: vscode.Uri,
                hunkId: string
            ) => {
                await runReviewCommand(
                    () =>
                        reviewManager.rejectHunk(
                            uri,
                            hunkId
                        )
                );
            }
        );

    const acceptAllCommand =
        vscode.commands.registerCommand(
            'code-bundler.acceptAllChanges',
            async () => {
                await runReviewCommand(
                    () =>
                        reviewManager.acceptAll()
                );
            }
        );

    const rejectAllCommand =
        vscode.commands.registerCommand(
            'code-bundler.rejectAllChanges',
            async () => {
                await runReviewCommand(
                    () =>
                        reviewManager.rejectAll()
                );
            }
        );

    context.subscriptions.push(
        outputChannel,
        bundleQueueManager,
        changeFileOpenPrompt,
        decorations,
        codeLensProvider,
        codeLensRegistration,
        fileDecorationRegistration,
        reviewContentRegistration,
        reviewFileListView,
        reviewFileListRegistration,

        registerPackCommand(
            outputChannel
        ),

        registerApplyChangeBundleCommand(
            reviewManager
        ),

        previousChangeCommand,
        previousPendingFileCommand,
        nextPendingFileCommand,
        openDiffCommand,
        nextChangeCommand,
        acceptCurrentCommand,
        rejectCurrentCommand,
        acceptHunkCommand,
        rejectHunkCommand,
        acceptAllCommand,
        rejectAllCommand
    );
}

export function deactivate(): void {
    // Resources are disposed through
    // context.subscriptions.
}

async function runReviewCommand(
    action: () => Promise<void>
): Promise<void> {
    try {
        await action();
    } catch (error) {
        const message =
            error instanceof Error
                ? error.message
                : String(error);

        vscode.window.showErrorMessage(
            vscode.l10n.t(
                'Change review failed: {0}',
                message
            )
        );
    }
}
<<<END_FILE path="src/extension.ts">>>

<<<BEGIN_FILE path="src/bundle/builder.ts" language="typescript" size="16.1 KB" sha256="9a7c44c82048fd897fa73ce58a1ca795f34088035f73839e098e1df1d19bc441">>>
import * as path from 'path';

import type {
    CollectResult,
    PackedFile
} from '../types';

import { buildRealFileTree } from './fileTree';
import { getLanguageFromFilePath } from './language';

import { formatBytes } from '../utils/format';
import {
    escapeMarkerAttribute,
    readTextFileContent
} from '../utils/fileSystem';
import { sha256Text } from '../utils/hash';
import { createChangeFileName } from '../constants';

export async function buildAiFriendlyBundle(
    collectResult: CollectResult,
    workspaceRoot: string,
    bundleId: string
): Promise<string> {
    const parts: string[] = [];
    const expectedChangeFileName =
        createChangeFileName(bundleId);

    const orderedPackedFiles =
        sortPackedFilesForAiReading(
            collectResult.packedFiles
        );

    const totalPackedBytes =
        orderedPackedFiles.reduce(
            (sum, file) => sum + file.sizeBytes,
            0
        );

    parts.push(
        `<<<CODE_BUNDLE bundle_id="${bundleId}" expected_change_file="${expectedChangeFileName}">>>`
    );
    parts.push('');
    parts.push('<<<AI_TASK_INSTRUCTIONS priority="highest">>>');
    parts.push(
        'This attachment is a complete source-code bundle, not a summary or excerpt.'
    );
    parts.push(
        'Before answering, use Python or equivalent file-processing tools to open and read the attachment itself from beginning to end. Do not rely only on an attachment preview, search snippet, extracted summary, or truncated chat context.'
    );
    parts.push(
        'Verify that each BEGIN_FILE marker has a matching END_FILE marker. If the complete attachment cannot be accessed, explicitly state that limitation instead of treating a visible excerpt as the whole file.'
    );
    parts.push(
        'For analysis requests, inspect the complete contents of all relevant file blocks and explain the project structure, relationships, and implementation details.'
    );
    parts.push(
        `For code-editing requests, follow the AI Editing Response Protocol below and return a downloadable plain-text CHANGE file named ${expectedChangeFileName} containing only complete changed-file blocks.`
    );
    parts.push(
        'Do not output partial snippets as the final edit result. Do not invent content that was not read from the attachment.'
    );
    parts.push('<<<END_AI_TASK_INSTRUCTIONS>>>');
    parts.push('');
    parts.push('# CODE BUNDLE FOR AI ANALYSIS');
    parts.push('');
    parts.push('## Bundle Identity');
    parts.push('');
    parts.push(`- Bundle ID: ${bundleId}`);
    parts.push(
        `- Required change file name: ${expectedChangeFileName}`
    );
    parts.push('');
    parts.push('## 1. Reading Instructions');
    parts.push('');
    parts.push(
        'This document is a complete-file source bundle generated from user-selected files and folders.'
    );
    parts.push('');
    parts.push(
        'AI analysis instruction: first open and read the complete attached file with Python or equivalent file-processing tools. Do not rely on a preview or truncated extraction. Then inspect every relevant BEGIN_FILE/END_FILE block, extract structure and relationships, and provide a detailed answer.'
    );
    parts.push('');
    parts.push('Important reading rules:');
    parts.push('');
    parts.push(
        '- Every included file is represented as a complete file.'
    );
    parts.push(
        '- No file content has been split into functions, classes, snippets, or summaries.'
    );
    parts.push(
        '- Each file starts with a BEGIN_FILE marker and ends with an END_FILE marker.'
    );
    parts.push(
        '- Each BEGIN_FILE marker includes a SHA-256 hash of the exact decoded text content.'
    );
    parts.push(
        '- The `path` attribute is the workspace-relative file path.'
    );
    parts.push(
        '- The `language` attribute is inferred from the file name or extension.'
    );
    parts.push(
        '- The `size` attribute is the original file size before bundling.'
    );
    parts.push(
        '- When analyzing this bundle, treat each file block as the authoritative full content of that file.'
    );
    parts.push(
        '- Some files and directories may have been skipped. See the final section, "Skipped Items".'
    );
    parts.push('');
    parts.push('Recommended analysis order for AI:');
    parts.push('');
    parts.push('1. Read the Bundle Overview.');
    parts.push('2. Read the Packed File Index.');
    parts.push('3. Read the Project File Tree.');
    parts.push(
        '4. Then read the Complete File Contents in order.'
    );
    parts.push(
        '5. Use the Skipped Items section only to understand what was intentionally excluded.'
    );
    parts.push('');
    parts.push('---');
    parts.push('');

    parts.push('## 2. Bundle Overview');
    parts.push('');
    parts.push(
        `- Workspace Root Name: ${path.basename(
            workspaceRoot
        )}`
    );
    parts.push(
        `- Generated At: ${new Date().toISOString()}`
    );
    parts.push(
        `- Total Scanned Files: ${collectResult.totalFiles}`
    );
    parts.push(
        `- Packed Complete Files: ${orderedPackedFiles.length}`
    );
    parts.push(
        `- Skipped Files: ${collectResult.skippedFiles.length}`
    );
    parts.push(
        `- Skipped Directories: ${collectResult.skippedDirs.length}`
    );
    parts.push(
        `- Packed Source Size: ${formatBytes(
            totalPackedBytes
        )}`
    );
    parts.push('- Bundle Format Version: 2.0');
    parts.push(
        '- File Content Mode: Complete files only'
    );
    parts.push('');
    parts.push('---');
    parts.push('');

    parts.push('## 3. Bundle Scope');
    parts.push('');
    parts.push(
        'This bundle contains the complete text content of files that passed the bundling filters.'
    );
    parts.push('');
    parts.push(
        'Included files are selected according to these rules:'
    );
    parts.push('');
    parts.push(
        '- Regular files are scanned recursively from the selected files and folders.'
    );
    parts.push(
        '- Files that look like text are included.'
    );
    parts.push('- Binary files are excluded.');
    parts.push('- Ignored folders are excluded.');
    parts.push('- Ignored files are excluded.');
    parts.push('- Oversized files are excluded.');
    parts.push(
        '- The generated bundle file itself is excluded.'
    );
    parts.push('');
    parts.push('This bundle does not contain:');
    parts.push('');
    parts.push(
        '- Dependency folders such as `node_modules`.'
    );
    parts.push('- Git metadata such as `.git`.');
    parts.push(
        '- Build output folders such as `dist`, `out`, or `build`.'
    );
    parts.push(
        '- Binary assets such as images, videos, archives, PDFs, or Office documents.'
    );
    parts.push(
        '- Files that could not be safely read as text.'
    );
    parts.push('');
    parts.push('---');
    parts.push('');

    parts.push('## 4. Packed File Index');
    parts.push('');
    parts.push(
        'The following files are included as complete files.'
    );
    parts.push('');
    parts.push('```text');

    for (const file of orderedPackedFiles) {
        parts.push(
            `${file.relativePath} | ` +
            `${getLanguageFromFilePath(
                file.relativePath
            )} | ` +
            `${formatBytes(file.sizeBytes)}`
        );
    }

    parts.push('```');
    parts.push('');
    parts.push('Index line format:');
    parts.push('');
    parts.push('```text');
    parts.push(
        'relative/path/to/file.ext | language | size'
    );
    parts.push('```');
    parts.push('');
    parts.push('---');
    parts.push('');

    parts.push('## 5. Project File Tree');
    parts.push('');
    parts.push(
        'The following tree only shows files included in this bundle.'
    );
    parts.push('');
    parts.push('```text');
    parts.push(
        ...buildRealFileTree(
            orderedPackedFiles.map(
                (file) => file.relativePath
            )
        )
    );
    parts.push('```');
    parts.push('');
    parts.push('---');
    parts.push('');

    parts.push('## 6. Complete File Contents');
    parts.push('');
    parts.push(
        'All files below are included in full.'
    );
    parts.push('');
    parts.push(
        'Each file uses this structure:'
    );
    parts.push('');
    parts.push('```text');
    parts.push(
        '<<<BEGIN_FILE path="src/example.ts" language="typescript" size="2.1 KB" sha256="...">>>'
    );
    parts.push('complete file content');
    parts.push(
        '<<<END_FILE path="src/example.ts">>>'
    );
    parts.push('```');
    parts.push('');
    parts.push(
        'Do not treat the markers as part of the original file content.'
    );
    parts.push('');

    parts.push('### 6.1 AI Editing Response Protocol');
    parts.push('');
    parts.push(
        'This protocol is mandatory for every code-editing response based on this attachment.'
    );
    parts.push('');
    parts.push(
        '1. Read the complete matching BEGIN_FILE block before editing it.'
    );
    parts.push(
        '2. Return the final result as a downloadable plain-text CHANGE file using the `code-change-<bundle-id>.txt` name specified in the bundle, not pasted fragments or prose only.'
    );
    parts.push(
        '3. Return only changed files using CHANGE blocks.'
    );
    parts.push(
        '4. Use operation="modify", "create", "delete", or "rename".'
    );
    parts.push(
        '5. For modify, delete, or rename, copy original_sha256 from the matching BEGIN_FILE marker.'
    );
    parts.push(
        '6. For rename, include new_path and do not include file content.'
    );
    parts.push(
        '7. For modify or create, put the complete resulting file content inside the block.'
    );
    parts.push(
        '8. Do not copy protocol examples or unchanged files into the response.'
    );
    parts.push('');
    parts.push('### 6.2 Save the AI Response');
    parts.push('');
    parts.push(
        '1. Save the returned CHANGE content using the `code-change-<bundle-id>.txt` name specified in the bundle.'
    );
    parts.push(
        '2. Do not remove or manually alter these markers:'
    );
    parts.push('');
    parts.push('```text');
    parts.push(
        '<<<BEGIN_CHANGE operation="modify" path="src/example.ts" original_sha256="<copy-from-BEGIN_FILE>">>>'
    );
    parts.push('complete resulting file content');
    parts.push(
        '<<<END_CHANGE path="src/example.ts">>>'
    );
    parts.push('```');
    parts.push('');
    parts.push('---');
    parts.push('');

    for (const file of orderedPackedFiles) {
        const content =
            await readTextFileContent(file.fullPath);

        const escapedRelativePath =
            escapeMarkerAttribute(
                file.relativePath
            );

        const language =
            getLanguageFromFilePath(
                file.relativePath
            );

        const contentSha256 =
            sha256Text(content);

        parts.push(
            `<<<BEGIN_FILE ` +
            `path="${escapedRelativePath}" ` +
            `language="${language}" ` +
            `size="${formatBytes(file.sizeBytes)}" ` +
            `sha256="${contentSha256}">>>`
        );

        parts.push(content);

        parts.push(
            `<<<END_FILE path="${escapedRelativePath}">>>`
        );

        parts.push('');
    }

    parts.push('---');
    parts.push('');

    parts.push('## 7. Skipped Items');
    parts.push('');
    parts.push(
        'This section lists files and directories that were scanned or encountered but not included in the complete file contents.'
    );
    parts.push('');
    parts.push(
        'Skipped items are provided for transparency only. They are not part of the source content available for analysis.'
    );
    parts.push('');

    parts.push('### 7.1 Skipped Files');
    parts.push('');
    parts.push('```text');

    if (collectResult.skippedFiles.length === 0) {
        parts.push('(none)');
    } else {
        for (
            const skipped of
            collectResult.skippedFiles
        ) {
            parts.push(
                `${skipped.relativePath} - ${skipped.reason}`
            );
        }
    }

    parts.push('```');
    parts.push('');
    parts.push('Skipped file line format:');
    parts.push('');
    parts.push('```text');
    parts.push(
        'relative/path/to/file.ext - reason'
    );
    parts.push('```');
    parts.push('');

    parts.push('### 7.2 Skipped Directories');
    parts.push('');
    parts.push('```text');

    if (collectResult.skippedDirs.length === 0) {
        parts.push('(none)');
    } else {
        for (
            const skipped of
            collectResult.skippedDirs
        ) {
            parts.push(
                `${skipped.relativePath} - ${skipped.reason}`
            );
        }
    }

    parts.push('```');
    parts.push('');
    parts.push(
        'Skipped directory line format:'
    );
    parts.push('');
    parts.push('```text');
    parts.push(
        'relative/path/to/directory - reason'
    );
    parts.push('```');
    parts.push('');
    parts.push('---');
    parts.push('');

    parts.push('## 8. End Of Bundle');
    parts.push('');
    parts.push(
        'This is the end of the complete-file source bundle.'
    );
    parts.push('');
    parts.push(
        'If a file is not listed in the Packed File Index or Complete File Contents section, assume it was not included in this bundle.'
    );
    parts.push('');

    return parts.join('\n');
}

function sortPackedFilesForAiReading(
    files: PackedFile[]
): PackedFile[] {
    return [...files].sort((fileA, fileB) => {
        const priorityA = getAiReadingPriority(
            fileA.relativePath
        );

        const priorityB = getAiReadingPriority(
            fileB.relativePath
        );

        if (priorityA !== priorityB) {
            return priorityA - priorityB;
        }

        return fileA.relativePath.localeCompare(
            fileB.relativePath
        );
    });
}

function getAiReadingPriority(
    relativePath: string
): number {
    const normalizedPath = relativePath
        .replace(/\\/g, '/')
        .toLowerCase();

    const fileName =
        path.basename(normalizedPath);

    if (fileName === 'package.json') {
        return 10;
    }

    if (
        fileName === 'readme.md' ||
        fileName === 'readme.txt' ||
        fileName === 'readme'
    ) {
        return 20;
    }

    if (
        fileName === 'tsconfig.json' ||
        fileName === 'jsconfig.json' ||
        fileName.startsWith('vite.config.') ||
        fileName.startsWith('webpack.config.') ||
        fileName.startsWith('rollup.config.') ||
        fileName.startsWith('eslint.config.')
    ) {
        return 30;
    }

    if (
        normalizedPath === 'src/extension.ts' ||
        normalizedPath === 'src/extension.js' ||
        normalizedPath === 'extension.ts' ||
        normalizedPath === 'extension.js'
    ) {
        return 40;
    }

    if (
        normalizedPath.startsWith('src/') ||
        normalizedPath.startsWith('lib/') ||
        normalizedPath.startsWith('app/')
    ) {
        return 50;
    }

    if (
        normalizedPath.startsWith('test/') ||
        normalizedPath.startsWith('tests/') ||
        normalizedPath.includes('/test/') ||
        normalizedPath.includes('/tests/') ||
        normalizedPath.endsWith('.test.ts') ||
        normalizedPath.endsWith('.test.js') ||
        normalizedPath.endsWith('.spec.ts') ||
        normalizedPath.endsWith('.spec.js')
    ) {
        return 70;
    }

    if (
        fileName.endsWith('.md') ||
        fileName.endsWith('.txt')
    ) {
        return 80;
    }

    return 100;
}
<<<END_FILE path="src/bundle/builder.ts">>>

<<<BEGIN_FILE path="src/bundle/bundleQueueManager.ts" language="typescript" size="7.1 KB" sha256="a3c934f3978c18c260846159aeb2a6507a363107b78cdcf268dd8a1217b68b17">>>
import * as vscode from 'vscode';

const queueStorageKey =
    'codeBundler.bundleQueue';
const queueContextKey =
    'codeBundler.bundleQueueNotEmpty';

export class BundleQueueManager
implements vscode.Disposable {
    private readonly queuedUris =
        new Map<string, vscode.Uri>();
    private readonly statusBarItem:
        vscode.StatusBarItem;
    private readonly disposables:
        vscode.Disposable[];

    public constructor(
        private readonly context:
            vscode.ExtensionContext
    ) {
        this.statusBarItem =
            vscode.window.createStatusBarItem(
                vscode.StatusBarAlignment.Right,
                100
            );
        this.statusBarItem.command =
            'code-bundler.manageBundleQueue';

        this.restoreQueue();

        this.disposables = [
            vscode.commands.registerCommand(
                'code-bundler.addToBundleQueue',
                (
                    uri?: vscode.Uri,
                    selectedUris?: vscode.Uri[]
                ) => this.add(uri, selectedUris)
            ),
            vscode.commands.registerCommand(
                'code-bundler.removeFromBundleQueue',
                () => this.removeWithPicker()
            ),
            vscode.commands.registerCommand(
                'code-bundler.packBundleQueue',
                () => this.pack()
            ),
            vscode.commands.registerCommand(
                'code-bundler.clearBundleQueue',
                () => this.clear()
            ),
            vscode.commands.registerCommand(
                'code-bundler.manageBundleQueue',
                () => this.showActions()
            )
        ];

        this.refreshUi();
    }

    public dispose(): void {
        this.statusBarItem.dispose();

        for (const disposable of this.disposables) {
            disposable.dispose();
        }
    }

    private async add(
        uri?: vscode.Uri,
        selectedUris?: vscode.Uri[]
    ): Promise<void> {
        const uris = selectedUris?.length
            ? selectedUris
            : uri
                ? [uri]
                : vscode.window.activeTextEditor
                    ? [vscode.window.activeTextEditor.document.uri]
                    : [];

        const fileUris = uris.filter(
            (item) => item.scheme === 'file'
        );

        for (const item of fileUris) {
            this.queuedUris.set(
                normalizeUri(item),
                item
            );
        }

        await this.persistAndRefresh();

        if (fileUris.length > 0) {
            vscode.window.setStatusBarMessage(
                vscode.l10n.t(
                    'Added {0} item(s) to the bundle queue.',
                    fileUris.length
                ),
                3000
            );
        }
    }

    private async showActions(): Promise<void> {
        const selected = await vscode.window.showQuickPick(
            [
                {
                    label: `$(package) ${vscode.l10n.t(
                        'Bundle queued items'
                    )}`,
                    command: 'code-bundler.packBundleQueue'
                },
                {
                    label: `$(list-selection) ${vscode.l10n.t(
                        'View or remove queued items'
                    )}`,
                    command: 'code-bundler.removeFromBundleQueue'
                },
                {
                    label: `$(clear-all) ${vscode.l10n.t(
                        'Clear bundle queue'
                    )}`,
                    command: 'code-bundler.clearBundleQueue'
                }
            ],
            {
                placeHolder: vscode.l10n.t(
                    'Choose a bundle queue action'
                )
            }
        );

        if (selected) {
            await vscode.commands.executeCommand(
                selected.command
            );
        }
    }

    private async removeWithPicker(): Promise<void> {
        const choices = Array.from(
            this.queuedUris.values()
        ).map((uri) => ({
            label: vscode.workspace.asRelativePath(
                uri,
                false
            ),
            uri
        }));

        const selected = await vscode.window.showQuickPick(
            choices,
            {
                canPickMany: true,
                placeHolder: vscode.l10n.t(
                    'Select queued items to remove'
                )
            }
        );

        if (!selected || selected.length === 0) {
            return;
        }

        for (const item of selected) {
            this.queuedUris.delete(
                normalizeUri(item.uri)
            );
        }

        await this.persistAndRefresh();
    }

    private async pack(): Promise<void> {
        const uris = Array.from(
            this.queuedUris.values()
        );

        if (uris.length === 0) {
            vscode.window.showWarningMessage(
                vscode.l10n.t(
                    'The bundle queue is empty.'
                )
            );
            return;
        }

        await vscode.commands.executeCommand(
            'code-bundler.packSelected',
            uris[0],
            uris
        );
    }

    private async clear(): Promise<void> {
        this.queuedUris.clear();
        await this.persistAndRefresh();

        vscode.window.setStatusBarMessage(
            vscode.l10n.t(
                'Bundle queue cleared.'
            ),
            3000
        );
    }

    private restoreQueue(): void {
        const storedUris = this.context.workspaceState.get<string[]>(
            queueStorageKey,
            []
        );

        for (const value of storedUris) {
            const uri = vscode.Uri.parse(value);
            this.queuedUris.set(
                normalizeUri(uri),
                uri
            );
        }
    }

    private async persistAndRefresh(): Promise<void> {
        await this.context.workspaceState.update(
            queueStorageKey,
            Array.from(
                this.queuedUris.values(),
                (uri) => uri.toString()
            )
        );

        this.refreshUi();
    }

    private refreshUi(): void {
        const count = this.queuedUris.size;

        void vscode.commands.executeCommand(
            'setContext',
            queueContextKey,
            count > 0
        );

        if (count === 0) {
            this.statusBarItem.hide();
            return;
        }

        this.statusBarItem.text =
            `$(package) ${vscode.l10n.t(
                'Bundle Queue: {0}',
                count
            )}`;
        this.statusBarItem.tooltip =
            vscode.l10n.t(
                'Click to manage the bundle queue.'
            );
        this.statusBarItem.show();
    }
}

function normalizeUri(
    uri: vscode.Uri
): string {
    return uri.fsPath
        .replace(/\\/g, '/')
        .toLowerCase();
}
<<<END_FILE path="src/bundle/bundleQueueManager.ts">>>

<<<BEGIN_FILE path="src/bundle/collector.ts" language="typescript" size="6.9 KB" sha256="e7bafb4e389c110d1e045edfdee2cf8a63a6f6b97b36d7f2f54f2441ebb1102c">>>
import * as vscode from 'vscode';
import * as fs from 'fs/promises';
import * as path from 'path';

import type {
    CollectResult,
    PackedFile
} from '../types';

import {
    ignoredDirs,
    ignoredFiles,
    isGeneratedExchangeFile,
    maxFileSizeBytes,
    obviousBinaryExtensions
} from '../constants';

import {
    containsNullByte,
    hasTextBom,
    hasTooManyControlBytes,
    looksLikeUtf16Text
} from '../utils/encoding';

import { formatBytes } from '../utils/format';
import { toRelativePath } from '../utils/fileSystem';

export async function getInputUris(
    uri?: vscode.Uri,
    selectedUris?: vscode.Uri[]
): Promise<vscode.Uri[]> {
    if (selectedUris && selectedUris.length > 0) {
        return selectedUris;
    }

    if (uri) {
        return [uri];
    }

    const pickedUris =
        await vscode.window.showOpenDialog({
            canSelectFiles: true,
            canSelectFolders: true,
            canSelectMany: true,
            title: vscode.l10n.t(
                'Select files or folders to bundle'
            )
        });

    return pickedUris ?? [];
}

export function getWorkspaceRoot(
    uri: vscode.Uri
): string | undefined {
    const workspaceFolder =
        vscode.workspace.getWorkspaceFolder(uri);

    if (workspaceFolder) {
        return workspaceFolder.uri.fsPath;
    }

    return vscode.workspace.workspaceFolders?.[0]
        ?.uri.fsPath;
}

export async function collectFromUris(
    uris: vscode.Uri[],
    workspaceRoot: string
): Promise<CollectResult> {
    const packedFileMap =
        new Map<string, PackedFile>();

    const result: CollectResult = {
        packedFiles: [],
        skippedFiles: [],
        skippedDirs: [],
        totalFiles: 0
    };

    for (const uri of uris) {
        await collectFileOrDirectory(
            uri.fsPath,
            workspaceRoot,
            packedFileMap,
            result
        );
    }

    result.packedFiles = Array.from(
        packedFileMap.values()
    ).sort((fileA, fileB) =>
        fileA.relativePath.localeCompare(
            fileB.relativePath
        )
    );

    result.skippedFiles.sort((itemA, itemB) =>
        itemA.relativePath.localeCompare(
            itemB.relativePath
        )
    );

    result.skippedDirs.sort((itemA, itemB) =>
        itemA.relativePath.localeCompare(
            itemB.relativePath
        )
    );

    return result;
}

async function collectFileOrDirectory(
    filePath: string,
    workspaceRoot: string,
    packedFileMap: Map<string, PackedFile>,
    result: CollectResult
): Promise<void> {
    let stat: Awaited<ReturnType<typeof fs.stat>>;

    try {
        stat = await fs.stat(filePath);
    } catch {
        result.skippedFiles.push({
            relativePath: toRelativePath(
                filePath,
                workspaceRoot
            ),
            reason: vscode.l10n.t(
                'Unable to read file status'
            )
        });

        return;
    }

    const relativePath = toRelativePath(
        filePath,
        workspaceRoot
    );

    if (stat.isDirectory()) {
        const directoryName = path.basename(filePath);

        if (ignoredDirs.has(directoryName)) {
            result.skippedDirs.push({
                relativePath,
                reason: vscode.l10n.t(
                    'Ignored directory'
                )
            });

            return;
        }

        let entries: string[];

        try {
            entries = await fs.readdir(filePath);
        } catch {
            result.skippedDirs.push({
                relativePath,
                reason: vscode.l10n.t(
                    'Unable to read directory'
                )
            });

            return;
        }

        for (const entry of entries) {
            await collectFileOrDirectory(
                path.join(filePath, entry),
                workspaceRoot,
                packedFileMap,
                result
            );
        }

        return;
    }

    if (!stat.isFile()) {
        result.skippedFiles.push({
            relativePath,
            reason: vscode.l10n.t(
                'Not a regular file'
            )
        });

        return;
    }

    result.totalFiles += 1;

    const check = await canPackAsText(
        filePath,
        stat.size
    );

    if (!check.canPack) {
        result.skippedFiles.push({
            relativePath,
            reason: check.reason
        });

        return;
    }

    packedFileMap.set(filePath, {
        fullPath: filePath,
        relativePath,
        sizeBytes: stat.size
    });
}

async function canPackAsText(
    filePath: string,
    sizeBytes: number
): Promise<
    | { canPack: true }
    | { canPack: false; reason: string }
> {
    const fileName = path.basename(filePath);
    const extension = path
        .extname(filePath)
        .toLowerCase();

    if (
        ignoredFiles.has(fileName) ||
        isGeneratedExchangeFile(fileName)
    ) {
        return {
            canPack: false,
            reason: vscode.l10n.t('Ignored file')
        };
    }

    if (obviousBinaryExtensions.has(extension)) {
        return {
            canPack: false,
            reason: vscode.l10n.t(
                'Obvious binary file'
            )
        };
    }

    if (sizeBytes > maxFileSizeBytes) {
        return {
            canPack: false,
            reason: vscode.l10n.t(
                'File is too large, larger than {0}',
                formatBytes(maxFileSizeBytes)
            )
        };
    }

    if (!(await looksLikeTextFile(filePath))) {
        return {
            canPack: false,
            reason: vscode.l10n.t(
                'Content does not look like a text file'
            )
        };
    }

    return {
        canPack: true
    };
}

async function looksLikeTextFile(
    filePath: string
): Promise<boolean> {
    const sampleSize = 8192;
    let fileHandle: fs.FileHandle | undefined;

    try {
        fileHandle = await fs.open(filePath, 'r');

        const buffer = Buffer.alloc(sampleSize);

        const { bytesRead } = await fileHandle.read(
            buffer,
            0,
            sampleSize,
            0
        );

        if (bytesRead === 0) {
            return true;
        }

        const sample = buffer.subarray(
            0,
            bytesRead
        );

        if (hasTextBom(sample)) {
            return true;
        }

        if (looksLikeUtf16Text(sample)) {
            return true;
        }

        if (containsNullByte(sample)) {
            return false;
        }

        if (hasTooManyControlBytes(sample)) {
            return false;
        }

        return true;
    } catch {
        return false;
    } finally {
        await fileHandle?.close();
    }
}
<<<END_FILE path="src/bundle/collector.ts">>>

<<<BEGIN_FILE path="src/bundle/fileTree.ts" language="typescript" size="3.1 KB" sha256="42f8e77eda39143004143901f6c112063aa504e4108e59fe3ddc38780b764e2d">>>
import type { FileTreeNode } from '../types';

export function buildRealFileTree(
    relativePaths: string[]
): string[] {
    const root: FileTreeNode = {
        directories: new Map<string, FileTreeNode>(),
        files: []
    };

    for (const relativePath of relativePaths) {
        const parts = relativePath
            .split('/')
            .filter(Boolean);

        if (parts.length === 0) {
            continue;
        }

        let currentNode = root;

        for (
            let index = 0;
            index < parts.length;
            index += 1
        ) {
            const part = parts[index];
            const isFile = index === parts.length - 1;

            if (isFile) {
                currentNode.files.push(part);
                continue;
            }

            let childNode =
                currentNode.directories.get(part);

            if (!childNode) {
                childNode = {
                    directories:
                        new Map<string, FileTreeNode>(),
                    files: []
                };

                currentNode.directories.set(
                    part,
                    childNode
                );
            }

            currentNode = childNode;
        }
    }

    const lines: string[] = ['.'];

    appendFileTreeNodeLines(root, lines, '');

    return lines;
}

function appendFileTreeNodeLines(
    node: FileTreeNode,
    lines: string[],
    prefix: string
): void {
    const directoryEntries = Array.from(
        node.directories.entries()
    ).sort(
        ([nameA], [nameB]) =>
            nameA.localeCompare(nameB)
    );

    const fileEntries = [...node.files].sort(
        (nameA, nameB) =>
            nameA.localeCompare(nameB)
    );

    const entries: Array<{
        name: string;
        type: 'directory' | 'file';
        node?: FileTreeNode;
    }> = [
        ...directoryEntries.map(
            ([name, childNode]) => ({
                name,
                type: 'directory' as const,
                node: childNode
            })
        ),
        ...fileEntries.map((name) => ({
            name,
            type: 'file' as const
        }))
    ];

    for (
        let index = 0;
        index < entries.length;
        index += 1
    ) {
        const entry = entries[index];
        const isLast =
            index === entries.length - 1;

        const connector =
            isLast ? '└── ' : '├── ';

        const childPrefix =
            isLast ? '    ' : '│   ';

        const displayName =
            entry.type === 'directory'
                ? `${entry.name}/`
                : entry.name;

        lines.push(
            `${prefix}${connector}${displayName}`
        );

        if (
            entry.type === 'directory' &&
            entry.node
        ) {
            appendFileTreeNodeLines(
                entry.node,
                lines,
                `${prefix}${childPrefix}`
            );
        }
    }
}
<<<END_FILE path="src/bundle/fileTree.ts">>>

<<<BEGIN_FILE path="src/bundle/language.ts" language="typescript" size="2.9 KB" sha256="54e9d7b8e3640365d14fe6647dc0629d95367ae37ff3edc5108ce8b0c1510d33">>>
import * as path from 'path';

export function getLanguageFromFilePath(
    relativePath: string
): string {
    const fileName = path.basename(relativePath).toLowerCase();
    const ext = path.extname(relativePath).toLowerCase();

    if (fileName === 'package.json') {
        return 'json';
    }

    if (
        fileName === 'tsconfig.json' ||
        fileName === 'jsconfig.json'
    ) {
        return 'jsonc';
    }

    if (
        fileName === '.eslintrc' ||
        fileName === '.prettierrc' ||
        fileName === '.babelrc'
    ) {
        return 'json';
    }

    if (
        fileName === 'dockerfile' ||
        fileName.startsWith('dockerfile.')
    ) {
        return 'dockerfile';
    }

    if (
        fileName === 'makefile' ||
        fileName === 'gnumakefile'
    ) {
        return 'makefile';
    }

    switch (ext) {
        case '.ts':
            return 'typescript';

        case '.tsx':
            return 'tsx';

        case '.js':
        case '.mjs':
        case '.cjs':
            return 'javascript';

        case '.jsx':
            return 'jsx';

        case '.json':
            return 'json';

        case '.jsonc':
            return 'jsonc';

        case '.md':
        case '.mdx':
            return 'markdown';

        case '.css':
            return 'css';

        case '.scss':
            return 'scss';

        case '.sass':
            return 'sass';

        case '.less':
            return 'less';

        case '.html':
        case '.htm':
            return 'html';

        case '.xml':
            return 'xml';

        case '.svg':
            return 'svg';

        case '.yml':
        case '.yaml':
            return 'yaml';

        case '.toml':
            return 'toml';

        case '.ini':
            return 'ini';

        case '.env':
            return 'dotenv';

        case '.sh':
        case '.bash':
        case '.zsh':
            return 'shell';

        case '.bat':
        case '.cmd':
            return 'batch';

        case '.ps1':
            return 'powershell';

        case '.py':
            return 'python';

        case '.java':
            return 'java';

        case '.c':
            return 'c';

        case '.cpp':
        case '.cc':
        case '.cxx':
        case '.h':
        case '.hpp':
            return 'cpp';

        case '.cs':
            return 'csharp';

        case '.go':
            return 'go';

        case '.rs':
            return 'rust';

        case '.php':
            return 'php';

        case '.rb':
            return 'ruby';

        case '.swift':
            return 'swift';

        case '.kt':
        case '.kts':
            return 'kotlin';

        case '.sql':
            return 'sql';

        case '.txt':
            return 'text';

        default:
            return 'text';
    }
}
<<<END_FILE path="src/bundle/language.ts">>>

<<<BEGIN_FILE path="src/changes/appliedReviewManager.ts" language="typescript" size="29.5 KB" sha256="5fffeeba3d8e3e515e393349e6bc0fdeb94a2716a6225cc149b858c550fe85f4">>>
import * as vscode from 'vscode';

import type { ChangeItem } from '../types';

import {
    AppliedReviewItem,
    AppliedReviewSession
} from './appliedSession';

import {
    PendingChangeDecorations
} from './decorations';

import {
    setReviewActive
} from './reviewContext';

import {
    acceptAllDiffs,
    acceptHunk,
    buildReviewedContent,
    createFileDiff,
    hasPendingHunks,
    rejectAllDiffs,
    rejectHunk
} from './diffHunks';

import {
    reviewCodeLensChangedEvent
} from './reviewCodeLens';

import {
    fileExists,
    readTextFileContent,
    resolveWorkspacePath
} from '../utils/fileSystem';

import { sha256Text } from '../utils/hash';
import { ReviewContentProvider } from './reviewContentProvider';

export class AppliedReviewManager {
    private session:
        AppliedReviewSession | undefined;

    public constructor(
        private readonly decorations:
            PendingChangeDecorations,
        private readonly outputChannel:
            vscode.OutputChannel,
        private readonly reviewContentProvider:
            ReviewContentProvider
    ) {}

    public get hasActiveSession(): boolean {
        return this.session !== undefined;
    }

    public async start(
        changes: ChangeItem[],
        workspaceRoot: string
    ): Promise<void> {
        if (this.session) {
            throw new Error(
                'A change review is already active.'
            );
        }

        const session =
            new AppliedReviewSession(
                changes,
                workspaceRoot
            );

        this.session = session;

        try {
            for (const item of session.allItems) {
                await this.captureSnapshot(item);
            }

            await this.applyChanges(session);
            await this.refreshAppliedSnapshots(
                session
            );

            await setReviewActive(true);

            this.markPendingFiles(session);
            this.showSessionSummary(session);

            await this.openFirstPendingFile();
        } catch (error) {
            await this.rollbackSession(session);
            await this.clearSession();

            throw error;
        }
    }

    public async previousChange():
        Promise<void> {
        await this.navigatePendingHunk(-1);
    }

    public async nextChange():
        Promise<void> {
        await this.navigatePendingHunk(1);
    }

    public async previousPendingFile():
        Promise<void> {
        await this.navigatePendingFile(-1);
    }

    public async nextPendingFile():
        Promise<void> {
        await this.navigatePendingFile(1);
    }

    public async openCurrentDiff(
        uri?: vscode.Uri
    ): Promise<void> {
        const item = this.getPendingItem(uri);

        if (!item) {
            throw new Error(
                'No pending review item exists for this file.'
            );
        }

        const filePath = this.getAppliedPath(item);
        const originalUri =
            this.reviewContentProvider.createUri(
                item.id,
                'original',
                filePath,
                item.originalContent ?? ''
            );
        const proposedUri =
            this.reviewContentProvider.createUri(
                item.id,
                'proposed',
                filePath,
                item.fileDiff
                    ? buildReviewedContent(item.fileDiff)
                    : item.appliedContent ?? ''
            );

        await vscode.commands.executeCommand(
            'vscode.diff',
            originalUri,
            proposedUri,
            `AI Review: ${item.change.path}`,
            { preview: false }
        );
    }

    public async acceptCurrent(
        uri?: vscode.Uri
    ): Promise<void> {
        const item =
            this.getPendingItem(uri);

        if (!item) {
            throw new Error(
                'There is no pending change for this file.'
            );
        }

        if (item.fileDiff) {
            acceptAllDiffs(item.fileDiff);
        }

        this.requireSession()
            .accept(item.id);

        this.clearItemDecoration(item);

        this.outputChannel.appendLine(
            `Accepted file: ${item.change.path}`
        );

        await this.continueOrFinish();
    }

    public async rejectCurrent(
        uri?: vscode.Uri
    ): Promise<void> {
        const item =
            this.getPendingItem(uri);

        if (!item) {
            throw new Error(
                'There is no pending change for this file.'
            );
        }

        await this.restoreItem(item);

        if (item.fileDiff) {
            rejectAllDiffs(item.fileDiff);
        }

        this.requireSession()
            .reject(item.id);

        this.clearItemDecoration(item);

        this.outputChannel.appendLine(
            `Rejected file: ${item.change.path}`
        );

        await this.continueOrFinish();
    }

    public async acceptHunk(
        uri: vscode.Uri,
        hunkId: string
    ): Promise<void> {
        const item =
            this.requireItemForUri(uri);

        if (!item.fileDiff) {
            throw new Error(
                'This file does not contain reviewable change blocks.'
            );
        }

        acceptHunk(
            item.fileDiff,
            hunkId
        );

        await this.updateFileFromDiff(item);
        await this.refreshItemState(item);
    }

    public async rejectHunk(
        uri: vscode.Uri,
        hunkId: string
    ): Promise<void> {
        const item =
            this.requireItemForUri(uri);

        if (!item.fileDiff) {
            throw new Error(
                'This file does not contain reviewable change blocks.'
            );
        }

        rejectHunk(
            item.fileDiff,
            hunkId
        );

        await this.updateFileFromDiff(item);
        await this.refreshItemState(item);
    }

    public async acceptAll():
        Promise<void> {
        const session =
            this.requireSession();

        for (const item of session.pendingItems) {
            if (item.fileDiff) {
                acceptAllDiffs(
                    item.fileDiff
                );
            }

            session.accept(item.id);
            this.clearItemDecoration(item);
        }

        await this.finishSession();
    }

    public async rejectAll():
        Promise<void> {
        const session =
            this.requireSession();

        const pendingItems = [
            ...session.pendingItems
        ].reverse();

        for (const item of pendingItems) {
            await this.restoreItem(item);

            if (item.fileDiff) {
                rejectAllDiffs(
                    item.fileDiff
                );
            }

            session.reject(item.id);
            this.clearItemDecoration(item);
        }

        await this.finishSession();
    }

    private async captureSnapshot(
        item: AppliedReviewItem
    ): Promise<void> {
        const session =
            this.requireSession();

        const change = item.change;

        if (change.operation === 'create') {
            session.setSnapshot(
                item.id,
                '',
                change.content,
                sha256Text(change.content)
            );

            session.setFileDiff(
                item.id,
                createFileDiff(
                    '',
                    change.content
                )
            );

            return;
        }

        const originalPath =
            resolveWorkspacePath(
                session.workspaceRoot,
                change.path
            );

        const originalContent =
            await readTextFileContent(
                originalPath
            );

        if (change.operation === 'delete') {
            session.setSnapshot(
                item.id,
                originalContent,
                undefined,
                undefined
            );

            session.setFileDiff(
                item.id,
                createFileDiff(
                    originalContent,
                    ''
                )
            );

            return;
        }

        if (change.operation === 'rename') {
            session.setSnapshot(
                item.id,
                originalContent,
                originalContent,
                sha256Text(originalContent)
            );

            return;
        }

        session.setSnapshot(
            item.id,
            originalContent,
            change.content,
            sha256Text(change.content)
        );

        session.setFileDiff(
            item.id,
            createFileDiff(
                originalContent,
                change.content
            )
        );
    }

    private async applyChanges(
        session: AppliedReviewSession
    ): Promise<void> {
        const edit =
            new vscode.WorkspaceEdit();

        for (const item of session.allItems) {
            const change = item.change;

            const originalUri =
                vscode.Uri.file(
                    resolveWorkspacePath(
                        session.workspaceRoot,
                        change.path
                    )
                );

            switch (change.operation) {
                case 'create':
                    edit.createFile(
                        originalUri,
                        {
                            overwrite: false,
                            ignoreIfExists: false
                        }
                    );

                    edit.insert(
                        originalUri,
                        new vscode.Position(0, 0),
                        change.content
                    );
                    break;

                case 'modify': {
                    const document =
                        await vscode.workspace
                            .openTextDocument(
                                originalUri
                            );

                    edit.replace(
                        originalUri,
                        fullDocumentRange(
                            document
                        ),
                        change.content
                    );
                    break;
                }

                case 'delete':
                    edit.deleteFile(
                        originalUri,
                        {
                            recursive: false,
                            ignoreIfNotExists: false
                        }
                    );
                    break;

                case 'rename': {
                    if (!change.newPath) {
                        throw new Error(
                            `Rename is missing new_path: ${change.path}`
                        );
                    }

                    const newUri =
                        vscode.Uri.file(
                            resolveWorkspacePath(
                                session.workspaceRoot,
                                change.newPath
                            )
                        );

                    edit.renameFile(
                        originalUri,
                        newUri,
                        {
                            overwrite: false,
                            ignoreIfExists: false
                        }
                    );
                    break;
                }
            }
        }

        const applied =
            await vscode.workspace.applyEdit(
                edit
            );

        if (!applied) {
            throw new Error(
                'VS Code rejected the workspace edit.'
            );
        }

        await this.saveAppliedDocuments(
            session
        );
    }

    private async saveAppliedDocuments(
        session: AppliedReviewSession
    ): Promise<void> {
        for (const item of session.allItems) {
            if (
                item.change.operation ===
                'delete'
            ) {
                continue;
            }

            const filePath =
                this.getAppliedPath(item);

            if (!(await fileExists(filePath))) {
                continue;
            }

            const document =
                await vscode.workspace
                    .openTextDocument(
                        vscode.Uri.file(filePath)
                    );

            if (document.isDirty) {
                await document.save();
            }
        }
    }

    private async refreshAppliedSnapshots(
        session: AppliedReviewSession
    ): Promise<void> {
        for (const item of session.allItems) {
            if (
                item.change.operation ===
                'delete'
            ) {
                continue;
            }

            const appliedPath =
                this.getAppliedPath(item);

            if (!(await fileExists(appliedPath))) {
                continue;
            }

            const actualContent =
                await readTextFileContent(
                    appliedPath
                );

            session.updateAppliedState(
                item.id,
                actualContent,
                sha256Text(actualContent)
            );
        }
    }

    private async updateFileFromDiff(
        item: AppliedReviewItem
    ): Promise<void> {
        if (!item.fileDiff) {
            return;
        }

        const uri =
            vscode.Uri.file(
                this.getAppliedPath(item)
            );

        const reviewedContent =
            buildReviewedContent(
                item.fileDiff
            );

        const document =
            await vscode.workspace
                .openTextDocument(uri);

        const edit =
            new vscode.WorkspaceEdit();

        edit.replace(
            uri,
            fullDocumentRange(document),
            reviewedContent
        );

        const applied =
            await vscode.workspace.applyEdit(
                edit
            );

        if (!applied) {
            throw new Error(
                'VS Code rejected the hunk review edit.'
            );
        }

        const updatedDocument =
            await vscode.workspace
                .openTextDocument(uri);

        await updatedDocument.save();

        const actualContent =
            updatedDocument.getText();

        this.requireSession()
            .updateAppliedState(
                item.id,
                actualContent,
                sha256Text(actualContent)
            );

        this.decorations.updatePending(
            uri,
            item.fileDiff
        );

        reviewCodeLensChangedEvent.fire();
    }

    private async refreshItemState(
        item: AppliedReviewItem
    ): Promise<void> {
        const session =
            this.requireSession();

        session.refreshDecisionFromDiff(
            item.id
        );

        if (
            item.fileDiff &&
            hasPendingHunks(item.fileDiff)
        ) {
            this.decorations.updatePending(
                vscode.Uri.file(
                    this.getAppliedPath(item)
                ),
                item.fileDiff
            );

            reviewCodeLensChangedEvent.fire();
            return;
        }

        this.clearItemDecoration(item);

        if (item.decision === 'rejected') {
            await this.restoreItem(item);
        }

        await this.continueOrFinish();
    }

    private markPendingFiles(
        session: AppliedReviewSession
    ): void {
        for (const item of session.pendingItems) {
            if (
                item.change.operation ===
                    'delete' ||
                !item.fileDiff
            ) {
                continue;
            }

            this.decorations.markPending(
                vscode.Uri.file(
                    this.getAppliedPath(item)
                ),
                item.fileDiff
            );
        }

        reviewCodeLensChangedEvent.fire();
    }

    private clearItemDecoration(
        item: AppliedReviewItem
    ): void {
        if (
            item.change.operation ===
            'delete'
        ) {
            return;
        }

        this.decorations.clearPending(
            vscode.Uri.file(
                this.getAppliedPath(item)
            )
        );

        reviewCodeLensChangedEvent.fire();
    }

    private getPendingItem(
        uri?: vscode.Uri
    ): AppliedReviewItem | undefined {
        const session = this.session;

        if (!session) {
            return undefined;
        }

        const targetUri =
            uri ??
            vscode.window
                .activeTextEditor
                ?.document.uri;

        if (targetUri) {
            const matchingItem =
                session.pendingItems.find(
                    (item) =>
                        this.getAppliedPath(
                            item
                        ) === targetUri.fsPath
                );

            if (matchingItem) {
                return matchingItem;
            }
        }

        return session.pendingItems[0];
    }

    private requireItemForUri(
        uri: vscode.Uri
    ): AppliedReviewItem {
        const item =
            this.getPendingItem(uri);

        if (!item) {
            throw new Error(
                'No pending review item exists for this file.'
            );
        }

        return item;
    }

    private getAppliedPath(
        item: AppliedReviewItem
    ): string {
        const session =
            this.requireSession();

        const relativePath =
            item.change.operation ===
                'rename'
                ? item.change.newPath
                : item.change.path;

        if (!relativePath) {
            throw new Error(
                `Unable to resolve applied path: ${item.change.path}`
            );
        }

        return resolveWorkspacePath(
            session.workspaceRoot,
            relativePath
        );
    }

    private async restoreItem(
        item: AppliedReviewItem
    ): Promise<void> {
        const session =
            this.requireSession();

        const change = item.change;

        const originalUri =
            vscode.Uri.file(
                resolveWorkspacePath(
                    session.workspaceRoot,
                    change.path
                )
            );

        switch (change.operation) {
            case 'create': {
                const edit =
                    new vscode.WorkspaceEdit();

                edit.deleteFile(
                    originalUri,
                    {
                        recursive: false,
                        ignoreIfNotExists: true
                    }
                );

                await this.applyRestoreEdit(
                    edit
                );
                break;
            }

            case 'modify': {
                if (
                    item.originalContent ===
                    undefined
                ) {
                    throw new Error(
                        `Missing original snapshot: ${change.path}`
                    );
                }

                const document =
                    await vscode.workspace
                        .openTextDocument(
                            originalUri
                        );

                const edit =
                    new vscode.WorkspaceEdit();

                edit.replace(
                    originalUri,
                    fullDocumentRange(document),
                    item.originalContent
                );

                await this.applyRestoreEdit(
                    edit
                );

                await document.save();
                break;
            }

            case 'delete': {
                if (
                    item.originalContent ===
                    undefined
                ) {
                    throw new Error(
                        `Missing original snapshot: ${change.path}`
                    );
                }

                const edit =
                    new vscode.WorkspaceEdit();

                edit.createFile(
                    originalUri,
                    {
                        overwrite: false,
                        ignoreIfExists: false
                    }
                );

                edit.insert(
                    originalUri,
                    new vscode.Position(0, 0),
                    item.originalContent
                );

                await this.applyRestoreEdit(
                    edit
                );

                const restoredDocument =
                    await vscode.workspace
                        .openTextDocument(
                            originalUri
                        );

                await restoredDocument.save();
                break;
            }

            case 'rename': {
                if (!change.newPath) {
                    throw new Error(
                        `Rename is missing new_path: ${change.path}`
                    );
                }

                const newUri =
                    vscode.Uri.file(
                        resolveWorkspacePath(
                            session.workspaceRoot,
                            change.newPath
                        )
                    );

                const edit =
                    new vscode.WorkspaceEdit();

                edit.renameFile(
                    newUri,
                    originalUri,
                    {
                        overwrite: false,
                        ignoreIfExists: false
                    }
                );

                await this.applyRestoreEdit(
                    edit
                );
                break;
            }
        }
    }

    private async applyRestoreEdit(
        edit: vscode.WorkspaceEdit
    ): Promise<void> {
        const restored =
            await vscode.workspace.applyEdit(
                edit
            );

        if (!restored) {
            throw new Error(
                'VS Code rejected the restore operation.'
            );
        }
    }

    private async navigatePendingHunk(
        direction: -1 | 1
    ): Promise<void> {
        const editor = vscode.window.activeTextEditor;

        if (!editor) {
            await this.navigatePendingFile(direction);
            return;
        }

        const item = this.getPendingItem(
            editor.document.uri
        );
        const fileDiff = item?.fileDiff;

        if (!fileDiff) {
            await this.navigatePendingFile(direction);
            return;
        }

        const anchors = fileDiff.hunks
            .filter((hunk) =>
                hunk.decision === 'pending'
            )
            .map((hunk) =>
                hunk.lines.find((line) =>
                    line.kind !== 'unchanged' &&
                    line.decision === 'pending'
                )
            )
            .filter((line) => line !== undefined)
            .map((line) =>
                Math.max(
                    0,
                    (line.proposedLineNumber ??
                        line.originalLineNumber ?? 1) - 1
                )
            )
            .sort((left, right) => left - right);

        if (anchors.length === 0) {
            await this.navigatePendingFile(direction);
            return;
        }

        const currentLine = editor.selection.active.line;
        const targetLine = direction > 0
            ? anchors.find((line) => line > currentLine) ??
                anchors[0]
            : [...anchors].reverse()
                .find((line) => line < currentLine) ??
                anchors[anchors.length - 1];
        const position = new vscode.Position(
            targetLine,
            0
        );

        editor.selection = new vscode.Selection(
            position,
            position
        );
        editor.revealRange(
            new vscode.Range(position, position),
            vscode.TextEditorRevealType.InCenter
        );
    }

    private async navigatePendingFile(
        direction: -1 | 1
    ): Promise<void> {
        const items = this.requireSession()
            .pendingItems.filter(
                (item) =>
                    item.change.operation !== 'delete'
            );

        if (items.length === 0) {
            return;
        }

        const activePath =
            vscode.window.activeTextEditor
                ?.document.uri.fsPath
                .toLowerCase();

        const currentIndex = items.findIndex(
            (item) =>
                this.getAppliedPath(item)
                    .toLowerCase() === activePath
        );

        const baseIndex = currentIndex >= 0
            ? currentIndex
            : direction > 0
                ? -1
                : 0;
        const targetIndex =
            (baseIndex + direction + items.length) %
            items.length;

        const document =
            await vscode.workspace.openTextDocument(
                vscode.Uri.file(
                    this.getAppliedPath(
                        items[targetIndex]
                    )
                )
            );

        await vscode.window.showTextDocument(
            document,
            {
                preview: false
            }
        );
    }

    private async openFirstPendingFile():
        Promise<void> {
        const item =
            this.requireSession()
                .pendingItems.find(
                    (candidate) =>
                        candidate.change
                            .operation !==
                        'delete'
                );

        if (!item) {
            return;
        }

        const document =
            await vscode.workspace
                .openTextDocument(
                    vscode.Uri.file(
                        this.getAppliedPath(item)
                    )
                );

        await vscode.window.showTextDocument(
            document,
            {
                preview: false
            }
        );
    }

    private async continueOrFinish():
        Promise<void> {
        const session =
            this.requireSession();

        if (session.isComplete) {
            await this.finishSession();
            return;
        }

        await this.openFirstPendingFile();
    }

    private async finishSession():
        Promise<void> {
        const session =
            this.requireSession();

        const acceptedCount =
            session.acceptedItems.length;

        const rejectedCount =
            session.rejectedItems.length;

        this.outputChannel.appendLine(
            `Accepted files: ${acceptedCount}`
        );

        this.outputChannel.appendLine(
            `Rejected files: ${rejectedCount}`
        );

        await this.clearSession();

        await vscode.window
            .showInformationMessage(
                `Review completed. Accepted: ` +
                `${acceptedCount}, rejected: ` +
                `${rejectedCount}.`
            );
    }

    private showSessionSummary(
        session: AppliedReviewSession
    ): void {
        this.outputChannel.clear();

        this.outputChannel.appendLine(
            'AI changes were applied.'
        );

        this.outputChannel.appendLine(
            'Review each Git-style change block or the entire file.'
        );

        for (const item of session.allItems) {
            this.outputChannel.appendLine(
                `${item.change.operation.toUpperCase()} ` +
                item.change.path
            );
        }

        this.outputChannel.show(true);
    }

    private async rollbackSession(
        session: AppliedReviewSession
    ): Promise<void> {
        for (
            const item of
            [...session.allItems].reverse()
        ) {
            try {
                await this.restoreItem(item);
            } catch {
                // Preserve the original error.
            }
        }
    }

    private async clearSession():
        Promise<void> {
        this.reviewContentProvider.clear();
        this.decorations.clearAll();
        this.session = undefined;

        reviewCodeLensChangedEvent.fire();

        await setReviewActive(false);
    }

    private requireSession():
        AppliedReviewSession {
        if (!this.session) {
            throw new Error(
                'There is no active review session.'
            );
        }

        return this.session;
    }
}

function fullDocumentRange(
    document: vscode.TextDocument
): vscode.Range {
    const lastLine =
        document.lineAt(
            document.lineCount - 1
        );

    return new vscode.Range(
        new vscode.Position(0, 0),
        lastLine.rangeIncludingLineBreak.end
    );
}
<<<END_FILE path="src/changes/appliedReviewManager.ts">>>

<<<BEGIN_FILE path="src/changes/appliedSession.ts" language="typescript" size="6.1 KB" sha256="371f17602e519268ef92cc94d2565d76a3854e24a7c4d14a32dd2419c377b77b">>>
import type {
    ChangeItem
} from '../types';

import type {
    FileDiff
} from './diffHunks';

export type AppliedDecision =
    | 'pending'
    | 'accepted'
    | 'rejected';

export type AppliedReviewItem = {
    id: string;

    change: ChangeItem;

    originalContent?: string;
    appliedContent?: string;
    appliedSha256?: string;

    fileDiff?: FileDiff;

    decision: AppliedDecision;
};

export class AppliedReviewSession {
    private readonly items:
        AppliedReviewItem[];

    public constructor(
        changes: ChangeItem[],
        public readonly workspaceRoot: string
    ) {
        this.items = changes.map(
            (change, index) => ({
                id: `change-${index + 1}`,
                change,
                decision: 'pending'
            })
        );
    }

    public get allItems():
        readonly AppliedReviewItem[] {
        return this.items;
    }

    public get pendingItems():
        AppliedReviewItem[] {
        return this.items.filter(
            (item) =>
                item.decision === 'pending'
        );
    }

    public get acceptedItems():
        AppliedReviewItem[] {
        return this.items.filter(
            (item) =>
                item.decision === 'accepted'
        );
    }

    public get rejectedItems():
        AppliedReviewItem[] {
        return this.items.filter(
            (item) =>
                item.decision === 'rejected'
        );
    }

    public get isComplete(): boolean {
        return this.pendingItems.length === 0;
    }

    public findItem(
        id: string
    ): AppliedReviewItem | undefined {
        return this.items.find(
            (item) => item.id === id
        );
    }

    public findItemByPath(
        filePath: string
    ): AppliedReviewItem | undefined {
        const normalizedPath =
            normalizePath(filePath);

        return this.items.find((item) => {
            const appliedPath =
                item.change.operation === 'rename'
                    ? item.change.newPath
                    : item.change.path;

            if (!appliedPath) {
                return false;
            }

            return normalizePath(
                appliedPath
            ) === normalizedPath;
        });
    }

    public setSnapshot(
        id: string,
        originalContent: string | undefined,
        appliedContent: string | undefined,
        appliedSha256: string | undefined
    ): void {
        const item = this.requireItem(id);

        item.originalContent =
            originalContent;

        item.appliedContent =
            appliedContent;

        item.appliedSha256 =
            appliedSha256;
    }

    public setFileDiff(
        id: string,
        fileDiff: FileDiff | undefined
    ): void {
        const item = this.requireItem(id);

        item.fileDiff = fileDiff;
    }

    public updateAppliedState(
        id: string,
        appliedContent: string,
        appliedSha256: string
    ): void {
        const item = this.requireItem(id);

        item.appliedContent =
            appliedContent;

        item.appliedSha256 =
            appliedSha256;
    }

    public accept(
        id: string
    ): void {
        const item = this.requireItem(id);

        item.decision = 'accepted';
    }

    public reject(
        id: string
    ): void {
        const item = this.requireItem(id);

        item.decision = 'rejected';
    }

    public markPending(
        id: string
    ): void {
        const item = this.requireItem(id);

        item.decision = 'pending';
    }

    public acceptAll(): void {
        for (const item of this.items) {
            if (item.decision === 'pending') {
                item.decision = 'accepted';
            }
        }
    }

    public rejectAll(): void {
        for (const item of this.items) {
            if (item.decision === 'pending') {
                item.decision = 'rejected';
            }
        }
    }

    public hasPendingLines(
        id: string
    ): boolean {
        const item = this.requireItem(id);

        if (!item.fileDiff) {
            return item.decision === 'pending';
        }

        return item.fileDiff.hunks.some(
            (hunk) =>
                hunk.lines.some(
                    (line) =>
                        line.kind !==
                            'unchanged' &&
                        line.decision ===
                            'pending'
                )
        );
    }

    public refreshDecisionFromDiff(
        id: string
    ): void {
        const item = this.requireItem(id);

        if (!item.fileDiff) {
            return;
        }

        const changedLines =
            item.fileDiff.hunks.flatMap(
                (hunk) =>
                    hunk.lines.filter(
                        (line) =>
                            line.kind !==
                            'unchanged'
                    )
            );

        if (changedLines.length === 0) {
            item.decision = 'accepted';
            return;
        }

        if (
            changedLines.every(
                (line) =>
                    line.decision ===
                    'accepted'
            )
        ) {
            item.decision = 'accepted';
            return;
        }

        if (
            changedLines.every(
                (line) =>
                    line.decision ===
                    'rejected'
            )
        ) {
            item.decision = 'rejected';
            return;
        }

        item.decision = 'pending';
    }

    private requireItem(
        id: string
    ): AppliedReviewItem {
        const item = this.findItem(id);

        if (!item) {
            throw new Error(
                `Unknown review item: ${id}`
            );
        }

        return item;
    }
}

function normalizePath(
    value: string
): string {
    return value
        .replace(/\\/g, '/')
        .toLowerCase();
}
<<<END_FILE path="src/changes/appliedSession.ts">>>

<<<BEGIN_FILE path="src/changes/changeFileOpenPrompt.ts" language="typescript" size="2.6 KB" sha256="ff36650efc4904cbe4241db3884d1aeb4132efbce4446451f063ee6788ebbe48">>>
import * as vscode from 'vscode';

const changeFileNamePattern =
    /^code-change-[a-f0-9]+\.txt$/i;

export class ChangeFileOpenPrompt
implements vscode.Disposable {
    private readonly promptedDocuments =
        new Set<string>();
    private readonly disposables:
        vscode.Disposable[];

    public constructor() {
        this.disposables = [
            vscode.workspace.onDidOpenTextDocument(
                (document) => {
                    void this.promptIfChangeFile(document);
                }
            ),
            vscode.workspace.onDidCloseTextDocument(
                (document) => {
                    this.promptedDocuments.delete(
                        document.uri.toString()
                    );
                }
            )
        ];

        for (const document of vscode.workspace.textDocuments) {
            void this.promptIfChangeFile(document);
        }
    }

    public dispose(): void {
        for (const disposable of this.disposables) {
            disposable.dispose();
        }
    }

    private async promptIfChangeFile(
        document: vscode.TextDocument
    ): Promise<void> {
        if (!isChangeFile(document)) {
            return;
        }

        const documentKey = document.uri.toString();

        if (this.promptedDocuments.has(documentKey)) {
            return;
        }

        this.promptedDocuments.add(documentKey);

        const applyLabel = vscode.l10n.t('Apply');
        const openNormallyLabel =
            vscode.l10n.t('Open Normally');
        const selection =
            await vscode.window.showInformationMessage(
                vscode.l10n.t(
                    'This looks like an AI CHANGE file. Apply it to the current workspace?'
                ),
                {
                    modal: true
                },
                applyLabel,
                openNormallyLabel
            );

        if (selection !== applyLabel) {
            return;
        }

        await vscode.commands.executeCommand(
            'code-bundler.applyChangeBundle',
            document.uri
        );
    }
}

function isChangeFile(
    document: vscode.TextDocument
): boolean {
    if (document.uri.scheme !== 'file') {
        return false;
    }

    const fileName = document.uri.path
        .split('/')
        .pop() ?? '';

    if (!changeFileNamePattern.test(fileName)) {
        return false;
    }

    const text = document.getText();

    return (
        text.includes('<<<BEGIN_CHANGE ') &&
        text.includes('<<<END_CHANGE ')
    );
}
<<<END_FILE path="src/changes/changeFileOpenPrompt.ts">>>

<<<BEGIN_FILE path="src/changes/decorations.ts" language="typescript" size="27.4 KB" sha256="621536855c0587f6d3494cd264bedf251a163b66c75f93f97edbf77e6d11c0e6">>>
import * as vscode from 'vscode';

import type {
    DiffHunk,
    DiffLine,
    FileDiff
} from './diffHunks';
import {
    createReviewedLineMap,
    getReviewedLineNumber
} from './reviewedLineMap';
import type {
    ReviewedLineMap
} from './reviewedLineMap';

type PendingFile = {
    uri: vscode.Uri;
    fileDiff: FileDiff;
};

type ChangeKind =
    | 'added'
    | 'removed'
    | 'modified';

type ChangeBlock = {
    kind: ChangeKind;
    lines: DiffLine[];
    startIndex: number;
    endIndex: number;
};

export class PendingChangeDecorations
implements vscode.Disposable,
vscode.FileDecorationProvider {
    private readonly addedDecoration:
        vscode.TextEditorDecorationType;

    private readonly modifiedDecoration:
        vscode.TextEditorDecorationType;

    private readonly removedDecoration:
        vscode.TextEditorDecorationType;

    private readonly pendingFiles =
        new Map<string, PendingFile>();

    private readonly fileDecorationChanged =
        new vscode.EventEmitter<
            vscode.Uri | vscode.Uri[] | undefined
        >();

    public readonly onDidChangeFileDecorations =
        this.fileDecorationChanged.event;

    private readonly disposables:
        vscode.Disposable[] = [];

    public constructor() {
        this.addedDecoration =
            vscode.window
                .createTextEditorDecorationType({
                    isWholeLine: true,
                    backgroundColor:
                        'rgba(63, 185, 80, 0.08)',
                    borderStyle: 'solid',
                    borderWidth: '0 0 0 2px',
                    borderColor: '#3fb950',
                    overviewRulerColor:
                        new vscode.ThemeColor(
                            'editorOverviewRuler.addedForeground'
                        ),
                    overviewRulerLane:
                        vscode.OverviewRulerLane.Left
                });

        this.modifiedDecoration =
            vscode.window
                .createTextEditorDecorationType({
                    isWholeLine: true,
                    backgroundColor:
                        'rgba(210, 153, 34, 0.08)',
                    borderStyle: 'solid',
                    borderWidth: '0 0 0 2px',
                    borderColor: '#d29922',
                    overviewRulerColor: '#d29922',
                    overviewRulerLane:
                        vscode.OverviewRulerLane.Left
                });

        this.removedDecoration =
            vscode.window
                .createTextEditorDecorationType({
                    isWholeLine: true,
                    backgroundColor:
                        'rgba(248, 81, 73, 0.08)',
                    borderStyle: 'solid',
                    borderWidth: '0 0 0 2px',
                    borderColor: '#f85149',
                    overviewRulerColor:
                        new vscode.ThemeColor(
                            'editorOverviewRuler.deletedForeground'
                        ),
                    overviewRulerLane:
                        vscode.OverviewRulerLane.Left
                });

        this.disposables.push(
            vscode.window
                .onDidChangeVisibleTextEditors(
                    () => {
                        this.refreshVisibleEditors();
                    }
                ),
            vscode.window
                .onDidChangeActiveTextEditor(
                    (editor) => {
                        if (editor) {
                            this.refreshEditor(editor);
                        }
                    }
                ),
            vscode.workspace
                .onDidChangeTextDocument(
                    (event) => {
                        const editor =
                            vscode.window
                                .visibleTextEditors
                                .find(
                                    (candidate) =>
                                        candidate.document ===
                                        event.document
                                );

                        if (editor) {
                            this.refreshEditor(editor);
                        }
                    }
                )
        );
    }

    public markPending(
        uri: vscode.Uri,
        fileDiff: FileDiff
    ): void {
        this.pendingFiles.set(
            uri.toString(),
            {
                uri,
                fileDiff
            }
        );
        this.fileDecorationChanged.fire(undefined);
        this.refreshVisibleEditors();
    }

    public updatePending(
        uri: vscode.Uri,
        fileDiff: FileDiff
    ): void {
        this.markPending(uri, fileDiff);
    }

    public clearPending(
        uri: vscode.Uri
    ): void {
        const removed =
            this.pendingFiles.delete(
                uri.toString()
            );

        if (removed) {
            this.fileDecorationChanged.fire(undefined);
        }

        this.refreshVisibleEditors();
    }

    public clearAll(): void {
        const uris = Array.from(
            this.pendingFiles.values(),
            (pendingFile) => pendingFile.uri
        );

        this.pendingFiles.clear();

        if (uris.length > 0) {
            this.fileDecorationChanged.fire(undefined);
        }

        this.refreshVisibleEditors();
    }

    public provideFileDecoration(
        uri: vscode.Uri
    ): vscode.FileDecoration | undefined {
        if (!this.isPendingOrParent(uri)) {
            return undefined;
        }

        return new vscode.FileDecoration(
            '✦',
            vscode.l10n.t(
                'Contains pending AI change review'
            ),
            new vscode.ThemeColor(
                'gitDecoration.modifiedResourceForeground'
            )
        );
    }

    private isPendingOrParent(
        uri: vscode.Uri
    ): boolean {
        if (this.isPending(uri)) {
            return true;
        }

        const directoryPath =
            normalizeFileSystemPath(uri.fsPath)
                .replace(/\/$/, '');
        const prefix = `${directoryPath}/`;

        return Array.from(
            this.pendingFiles.values()
        ).some((pending) =>
            normalizeFileSystemPath(
                pending.uri.fsPath
            ).startsWith(prefix)
        );
    }

    public getPendingUris():
        vscode.Uri[] {
        return Array.from(
            this.pendingFiles.values(),
            (pending) => pending.uri
        );
    }

    public getFileDiff(
        uri: vscode.Uri
    ): FileDiff | undefined {
        return this.pendingFiles.get(
            uri.toString()
        )?.fileDiff;
    }

    public isPending(
        uri: vscode.Uri
    ): boolean {
        return this.pendingFiles.has(
            uri.toString()
        );
    }

    public refreshVisibleEditors(): void {
        for (
            const editor of
            vscode.window.visibleTextEditors
        ) {
            this.refreshEditor(editor);
        }
    }

    public dispose(): void {
        this.clearAll();
        this.addedDecoration.dispose();
        this.modifiedDecoration.dispose();
        this.removedDecoration.dispose();
        this.fileDecorationChanged.dispose();

        for (const disposable of this.disposables) {
            disposable.dispose();
        }
    }

    private refreshEditor(
        editor: vscode.TextEditor
    ): void {
        const pendingFile =
            this.pendingFiles.get(
                editor.document.uri.toString()
            );

        if (!pendingFile) {
            this.clearEditorDecorations(editor);
            return;
        }

        const reviewedLineMap =
            createReviewedLineMap(
                pendingFile.fileDiff
            );
        const added:
            vscode.DecorationOptions[] = [];
        const modified:
            vscode.DecorationOptions[] = [];
        const removed:
            vscode.DecorationOptions[] = [];

        for (
            const hunk of
            pendingFile.fileDiff.hunks
        ) {
            for (
                const block of
                createChangeBlocks(hunk)
            ) {
                appendBlockDecorations(
                    editor.document,
                    reviewedLineMap,
                    pendingFile.fileDiff,
                    hunk,
                    block,
                    added,
                    modified,
                    removed
                );
            }
        }

        editor.setDecorations(
            this.addedDecoration,
            added
        );
        editor.setDecorations(
            this.modifiedDecoration,
            modified
        );
        editor.setDecorations(
            this.removedDecoration,
            removed
        );
    }

    private clearEditorDecorations(
        editor: vscode.TextEditor
    ): void {
        editor.setDecorations(
            this.addedDecoration,
            []
        );
        editor.setDecorations(
            this.modifiedDecoration,
            []
        );
        editor.setDecorations(
            this.removedDecoration,
            []
        );
    }
}

function normalizeFileSystemPath(
    filePath: string
): string {
    return filePath
        .replace(/\\/g, '/')
        .toLowerCase();
}

function appendBlockDecorations(
    document: vscode.TextDocument,
    reviewedLineMap: ReviewedLineMap,
    fileDiff: FileDiff,
    hunk: DiffHunk,
    block: ChangeBlock,
    added: vscode.DecorationOptions[],
    modified: vscode.DecorationOptions[],
    removed: vscode.DecorationOptions[]
): void {
    const hoverMessage =
        createBlockHover(fileDiff, block);

    if (block.kind === 'removed') {
        removed.push({
            range: getRemovedAnchorRange(
                document,
                reviewedLineMap,
                hunk.lines,
                block
            ),
            hoverMessage
        });
        return;
    }

    const ranges = getCurrentRanges(
        document,
        reviewedLineMap,
        block.lines
    );
    const target =
        block.kind === 'added'
            ? added
            : modified;

    for (const range of ranges) {
        target.push({
            range,
            hoverMessage
        });
    }
}

function createChangeBlocks(
    hunk: DiffHunk
): ChangeBlock[] {
    const rawBlocks: ChangeBlock[] = [];

    let index = 0;

    while (index < hunk.lines.length) {
        const line = hunk.lines[index];

        if (
            line.kind === 'unchanged' ||
            line.decision !== 'pending'
        ) {
            index += 1;
            continue;
        }

        const kind = line.kind as ChangeKind;
        const startIndex = index;
        const lines: DiffLine[] = [];

        while (
            index < hunk.lines.length &&
            hunk.lines[index].kind === kind &&
            hunk.lines[index].decision ===
                'pending'
        ) {
            lines.push(hunk.lines[index]);
            index += 1;
        }

        rawBlocks.push({
            kind,
            lines,
            startIndex,
            endIndex: index
        });
    }

    return reconcileChangeBlocks(rawBlocks);
}

function reconcileChangeBlocks(
    blocks: ChangeBlock[]
): ChangeBlock[] {
    const addedEntries = collectBlockLines(
        blocks,
        'added'
    );
    const removedEntries = collectBlockLines(
        blocks,
        'removed'
    );
    const pairings = findBestLinePairings(
        removedEntries,
        addedEntries
    );

    const modifiedByAddedLine = new Map<
        DiffLine,
        DiffLine
    >();
    const pairedRemovedLines = new Set<
        DiffLine
    >();

    for (const pairing of pairings) {
        const removedEntry =
            removedEntries[
                pairing.removedIndex
            ];
        const addedEntry =
            addedEntries[
                pairing.addedIndex
            ];

        pairedRemovedLines.add(
            removedEntry.line
        );
        modifiedByAddedLine.set(
            addedEntry.line,
            {
                ...addedEntry.line,
                kind: 'modified',
                originalLine:
                    removedEntry.line
                        .originalLine,
                originalLineNumber:
                    removedEntry.line
                        .originalLineNumber
            }
        );
    }

    const result: ChangeBlock[] = [];

    for (const block of blocks) {
        if (block.kind === 'added') {
            appendReconciledAddedBlock(
                result,
                block,
                modifiedByAddedLine
            );
            continue;
        }

        if (block.kind === 'removed') {
            const remainingLines =
                block.lines.filter(
                    (line) =>
                        !pairedRemovedLines
                            .has(line)
                );

            if (remainingLines.length > 0) {
                result.push({
                    ...block,
                    lines: remainingLines
                });
            }
            continue;
        }

        result.push(block);
    }

    return result;
}

type BlockLineEntry = {
    line: DiffLine;
    blockIndex: number;
    lineIndex: number;
};

function collectBlockLines(
    blocks: ChangeBlock[],
    kind: 'added' | 'removed'
): BlockLineEntry[] {
    const entries: BlockLineEntry[] = [];

    blocks.forEach(
        (block, blockIndex) => {
            if (block.kind !== kind) {
                return;
            }

            block.lines.forEach(
                (line, lineIndex) => {
                    entries.push({
                        line,
                        blockIndex,
                        lineIndex
                    });
                }
            );
        }
    );

    return entries;
}

function appendReconciledAddedBlock(
    result: ChangeBlock[],
    block: ChangeBlock,
    modifiedByAddedLine: Map<
        DiffLine,
        DiffLine
    >
): void {
    let currentKind:
        'added' | 'modified' | undefined;
    let currentLines: DiffLine[] = [];

    const flush = (): void => {
        if (!currentKind || currentLines.length === 0) {
            return;
        }

        result.push({
            kind: currentKind,
            lines: currentLines,
            startIndex: block.startIndex,
            endIndex: block.endIndex
        });

        currentLines = [];
    };

    for (const line of block.lines) {
        const modifiedLine =
            modifiedByAddedLine.get(line);
        const nextKind = modifiedLine
            ? 'modified'
            : 'added';

        if (
            currentKind &&
            currentKind !== nextKind
        ) {
            flush();
        }

        currentKind = nextKind;
        currentLines.push(
            modifiedLine ?? line
        );
    }

    flush();
}

type LinePairing = {
    removedIndex: number;
    addedIndex: number;
    similarity: number;
};

function findBestLinePairings(
    removedEntries: BlockLineEntry[],
    addedEntries: BlockLineEntry[]
): LinePairing[] {
    const candidates: LinePairing[] = [];

    for (
        let removedIndex = 0;
        removedIndex < removedEntries.length;
        removedIndex += 1
    ) {
        for (
            let addedIndex = 0;
            addedIndex < addedEntries.length;
            addedIndex += 1
        ) {
            const removedText =
                removedEntries[removedIndex]
                    .line.originalLine ?? '';
            const addedText =
                addedEntries[addedIndex]
                    .line.proposedLine ?? '';

            if (
                isBlankLine(removedText) ||
                isBlankLine(addedText)
            ) {
                continue;
            }

            const similarity =
                calculateLineSimilarity(
                    removedText,
                    addedText
                );

            if (similarity >= 0.25) {
                candidates.push({
                    removedIndex,
                    addedIndex,
                    similarity
                });
            }
        }
    }

    candidates.sort(
        (left, right) => {
            if (
                right.similarity !==
                left.similarity
            ) {
                return right.similarity -
                    left.similarity;
            }

            return getPairDistance(
                removedEntries[
                    left.removedIndex
                ],
                addedEntries[
                    left.addedIndex
                ]
            ) - getPairDistance(
                removedEntries[
                    right.removedIndex
                ],
                addedEntries[
                    right.addedIndex
                ]
            );
        }
    );

    const usedRemoved = new Set<number>();
    const usedAdded = new Set<number>();
    const pairings: LinePairing[] = [];

    for (const candidate of candidates) {
        if (
            usedRemoved.has(
                candidate.removedIndex
            ) ||
            usedAdded.has(
                candidate.addedIndex
            )
        ) {
            continue;
        }

        usedRemoved.add(
            candidate.removedIndex
        );
        usedAdded.add(
            candidate.addedIndex
        );
        pairings.push(candidate);
    }

    return pairings;
}

function getPairDistance(
    removedEntry: BlockLineEntry,
    addedEntry: BlockLineEntry
): number {
    return Math.abs(
        removedEntry.blockIndex -
        addedEntry.blockIndex
    ) + Math.abs(
        removedEntry.lineIndex -
        addedEntry.lineIndex
    );
}

function calculateLineSimilarity(
    originalLine: string,
    proposedLine: string
): number {
    const original = normalizeLine(
        originalLine
    );
    const proposed = normalizeLine(
        proposedLine
    );

    if (
        original.length === 0 ||
        proposed.length === 0
    ) {
        return 0;
    }

    if (original === proposed) {
        return 1;
    }

    const distance =
        calculateLevenshteinDistance(
            original,
            proposed
        );

    return 1 - distance /
        Math.max(
            original.length,
            proposed.length
        );
}

function normalizeLine(
    line: string
): string {
    return line
        .trim()
        .replace(/\s+/g, ' ')
        .toLowerCase();
}

function isBlankLine(
    line: string
): boolean {
    return line.trim().length === 0;
}

function calculateLevenshteinDistance(
    left: string,
    right: string
): number {
    const previousRow = Array.from(
        {
            length: right.length + 1
        },
        (_, index) => index
    );
    const currentRow = new Array<number>(
        right.length + 1
    );

    for (
        let leftIndex = 1;
        leftIndex <= left.length;
        leftIndex += 1
    ) {
        currentRow[0] = leftIndex;

        for (
            let rightIndex = 1;
            rightIndex <= right.length;
            rightIndex += 1
        ) {
            const substitutionCost =
                left[leftIndex - 1] ===
                right[rightIndex - 1]
                    ? 0
                    : 1;

            currentRow[rightIndex] = Math.min(
                currentRow[rightIndex - 1] + 1,
                previousRow[rightIndex] + 1,
                previousRow[rightIndex - 1] +
                    substitutionCost
            );
        }

        for (
            let index = 0;
            index <= right.length;
            index += 1
        ) {
            previousRow[index] =
                currentRow[index];
        }
    }

    return previousRow[right.length];
}

function getCurrentRanges(
    document: vscode.TextDocument,
    reviewedLineMap: ReviewedLineMap,
    lines: DiffLine[]
): vscode.Range[] {
    const ranges: vscode.Range[] = [];
    const seenLines = new Set<number>();

    for (const line of lines) {
        if (line.proposedLine === undefined) {
            continue;
        }

        const lineNumber = clampLineNumber(
            document,
            getReviewedLineNumber(
                reviewedLineMap,
                line
            )
        );

        if (seenLines.has(lineNumber)) {
            continue;
        }

        seenLines.add(lineNumber);
        ranges.push(
            document.lineAt(lineNumber).range
        );
    }

    return ranges;
}

function getRemovedAnchorRange(
    document: vscode.TextDocument,
    reviewedLineMap: ReviewedLineMap,
    lines: DiffLine[],
    block: ChangeBlock
): vscode.Range {
    const anchor =
        findPreviousVisibleLine(
            lines,
            block.startIndex
        ) ??
        findNextVisibleLine(
            lines,
            block.endIndex
        ) ??
        block.lines[0];

    const lineNumber = clampLineNumber(
        document,
        getReviewedLineNumber(
            reviewedLineMap,
            anchor
        )
    );

    return document.lineAt(lineNumber).range;
}

function findPreviousVisibleLine(
    lines: DiffLine[],
    startIndex: number
): DiffLine | undefined {
    for (
        let index = startIndex - 1;
        index >= 0;
        index -= 1
    ) {
        const line = lines[index];

        if (
            isVisibleReviewedLine(line) &&
            getVisibleText(line).trim().length > 0
        ) {
            return line;
        }
    }

    return undefined;
}

function findNextVisibleLine(
    lines: DiffLine[],
    startIndex: number
): DiffLine | undefined {
    for (
        let index = startIndex;
        index < lines.length;
        index += 1
    ) {
        const line = lines[index];

        if (
            isVisibleReviewedLine(line) &&
            getVisibleText(line).trim().length > 0
        ) {
            return line;
        }
    }

    return undefined;
}

function isVisibleReviewedLine(
    line: DiffLine
): boolean {
    if (line.kind === 'unchanged') {
        return true;
    }

    if (line.decision === 'rejected') {
        return line.originalLine !== undefined;
    }

    return line.proposedLine !== undefined;
}

function getVisibleText(
    line: DiffLine
): string {
    if (
        line.kind !== 'unchanged' &&
        line.decision === 'rejected'
    ) {
        return line.originalLine ?? '';
    }

    return line.proposedLine ??
        line.originalLine ??
        '';
}

function createBlockHover(
    fileDiff: FileDiff,
    block: ChangeBlock
): vscode.MarkdownString {
    const contextSize = 5;
    const context = getBlockContext(
        fileDiff,
        block,
        contextSize
    );
    const previewLines = [
        ...context.before.map(
            (line) => `  ${line}`
        ),
        ...createChangedPreviewLines(block),
        ...context.after.map(
            (line) => `  ${line}`
        )
    ];

    const markdown =
        new vscode.MarkdownString();

    markdown.appendMarkdown(
        `**${getBlockTitle(block)}**\n\n`
    );

    markdown.appendCodeblock(
        previewLines.join('\n'),
        'diff'
    );

    markdown.appendMarkdown(
        `\n\n${vscode.l10n.t(
            'Accept or reject this entire change block.'
        )}`
    );

    return markdown;
}

type BlockContext = {
    before: string[];
    after: string[];
};

function getBlockContext(
    fileDiff: FileDiff,
    block: ChangeBlock,
    contextSize: number
): BlockContext {
    const useOriginalContent =
        block.kind === 'removed';
    const content = useOriginalContent
        ? fileDiff.originalContent
        : fileDiff.proposedContent;
    const lineNumbers = block.lines
        .map(
            (line) =>
                useOriginalContent
                    ? line.originalLineNumber
                    : line.proposedLineNumber
        )
        .filter(
            (lineNumber):
                lineNumber is number =>
                lineNumber !== undefined
        );

    if (lineNumbers.length === 0) {
        return {
            before: [],
            after: []
        };
    }

    const lines = splitContentLines(content);
    const firstLineNumber = Math.min(
        ...lineNumbers
    );
    const lastLineNumber = Math.max(
        ...lineNumbers
    );
    const beforeStart = Math.max(
        0,
        firstLineNumber - contextSize - 1
    );
    const beforeEnd = Math.max(
        0,
        firstLineNumber - 1
    );
    const afterStart = Math.min(
        lines.length,
        lastLineNumber
    );
    const afterEnd = Math.min(
        lines.length,
        lastLineNumber + contextSize
    );

    return {
        before: lines.slice(
            beforeStart,
            beforeEnd
        ),
        after: lines.slice(
            afterStart,
            afterEnd
        )
    };
}

function splitContentLines(
    content: string
): string[] {
    if (content.length === 0) {
        return [];
    }

    const lines = content.split(/\r?\n/);

    if (lines[lines.length - 1] === '') {
        lines.pop();
    }

    return lines;
}

function createChangedPreviewLines(
    block: ChangeBlock
): string[] {
    if (block.kind === 'added') {
        return block.lines.map(
            (line) =>
                `+ ${line.proposedLine ?? ''}`
        );
    }

    if (block.kind === 'removed') {
        return block.lines.map(
            (line) =>
                `- ${line.originalLine ?? ''}`
        );
    }

    const previewLines: string[] = [];

    for (const line of block.lines) {
        previewLines.push(
            `- ${line.originalLine ?? ''}`
        );
        previewLines.push(
            `+ ${line.proposedLine ?? ''}`
        );
    }

    return previewLines;
}

function collectContextLines(
    lines: DiffLine[]
): string[] {
    return lines
        .filter(isVisibleReviewedLine)
        .map(getVisibleText);
}

function getBlockTitle(
    block: ChangeBlock
): string {
    switch (block.kind) {
        case 'added':
            return vscode.l10n.t(
                'AI added {0} lines',
                block.lines.length
            );
        case 'removed':
            return vscode.l10n.t(
                'AI deleted {0} lines',
                block.lines.length
            );
        case 'modified':
            return vscode.l10n.t(
                'AI modified {0} lines',
                block.lines.length
            );
    }
}

function clampLineNumber(
    document: vscode.TextDocument,
    lineNumber: number
): number {
    return Math.max(
        0,
        Math.min(
            lineNumber,
            document.lineCount - 1
        )
    );
}
<<<END_FILE path="src/changes/decorations.ts">>>

<<<BEGIN_FILE path="src/changes/diffHunks.ts" language="typescript" size="24.7 KB" sha256="ad454ec13eae8c3130b4c990cc69ef065fc42df1da819c6a8b96fab4d70edbcc">>>
export type HunkDecision =
    | 'pending'
    | 'accepted'
    | 'rejected';

export type DiffLineKind =
    | 'unchanged'
    | 'added'
    | 'removed'
    | 'modified';

export type DiffLine = {
    id: string;
    kind: DiffLineKind;

    originalLine?: string;
    proposedLine?: string;

    originalLineNumber?: number;
    proposedLineNumber?: number;

    decision: HunkDecision;
};

export type DiffHunk = {
    id: string;
    lines: DiffLine[];
    decision: HunkDecision;
};

export type FileDiff = {
    originalContent: string;
    proposedContent: string;
    lineEnding: string;
    hunks: DiffHunk[];
};

type DiffOperation = {
    kind:
    | 'equal'
    | 'add'
    | 'remove';

    text: string;

    originalLineNumber?: number;
    proposedLineNumber?: number;
};

const maximumUnchangedLinesInsideHunk = 3;

export function createFileDiff(
    originalContent: string,
    proposedContent: string
): FileDiff {
    const lineEnding =
        detectLineEnding(
            proposedContent ||
            originalContent
        );

    const originalLines =
        splitLines(originalContent);

    const proposedLines =
        splitLines(proposedContent);

    const operations =
        createOperations(
            originalLines,
            proposedLines
        );

    const diffLines =
        createDiffLines(operations);

    return {
        originalContent,
        proposedContent,
        lineEnding,
        hunks:
            groupIntoHunks(diffLines)
    };
}

export function buildReviewedContent(
    fileDiff: FileDiff
): string {
    const outputLines: string[] = [];

    for (const hunk of fileDiff.hunks) {
        for (const line of hunk.lines) {
            appendReviewedLine(
                outputLines,
                line
            );
        }
    }

    return outputLines.join(
        fileDiff.lineEnding
    );
}

export function acceptHunk(
    fileDiff: FileDiff,
    hunkId: string
): void {
    const hunk =
        requireHunk(
            fileDiff,
            hunkId
        );

    for (const line of hunk.lines) {
        if (line.kind !== 'unchanged') {
            line.decision = 'accepted';
        }
    }

    refreshHunkDecision(hunk);
}

export function rejectHunk(
    fileDiff: FileDiff,
    hunkId: string
): void {
    const hunk =
        requireHunk(
            fileDiff,
            hunkId
        );

    for (const line of hunk.lines) {
        if (line.kind !== 'unchanged') {
            line.decision = 'rejected';
        }
    }

    refreshHunkDecision(hunk);
}

export function acceptAllDiffs(
    fileDiff: FileDiff
): void {
    for (const hunk of fileDiff.hunks) {
        if (hasChanges(hunk)) {
            acceptHunk(
                fileDiff,
                hunk.id
            );
        }
    }
}

export function rejectAllDiffs(
    fileDiff: FileDiff
): void {
    for (const hunk of fileDiff.hunks) {
        if (hasChanges(hunk)) {
            rejectHunk(
                fileDiff,
                hunk.id
            );
        }
    }
}

export function getPendingHunks(
    fileDiff: FileDiff
): DiffHunk[] {
    return fileDiff.hunks.filter(
        (hunk) =>
            hunk.decision === 'pending' &&
            hasChanges(hunk)
    );
}

export function hasPendingHunks(
    fileDiff: FileDiff
): boolean {
    return getPendingHunks(
        fileDiff
    ).length > 0;
}

export function countChangedLines(
    hunk: DiffHunk
): number {
    return hunk.lines.filter(
        (line) =>
            line.kind !== 'unchanged'
    ).length;
}

export function getChangedLines(
    hunk: DiffHunk
): DiffLine[] {
    return hunk.lines.filter(
        (line) =>
            line.kind !== 'unchanged'
    );
}

function detectLineEnding(
    content: string
): string {
    return content.includes('\r\n')
        ? '\r\n'
        : '\n';
}

function splitLines(
    content: string
): string[] {
    if (content.length === 0) {
        return [];
    }

    return content
        .replace(/\r\n/g, '\n')
        .split('\n');
}

function createOperations(
    originalLines: string[],
    proposedLines: string[]
): DiffOperation[] {
    const originalCount =
        originalLines.length;

    const proposedCount =
        proposedLines.length;

    const matrix: number[][] =
        Array.from(
            {
                length:
                    originalCount + 1
            },
            () =>
                new Array<number>(
                    proposedCount + 1
                ).fill(0)
        );

    for (
        let originalIndex =
            originalCount - 1;
        originalIndex >= 0;
        originalIndex -= 1
    ) {
        for (
            let proposedIndex =
                proposedCount - 1;
            proposedIndex >= 0;
            proposedIndex -= 1
        ) {
            if (
                originalLines[
                originalIndex
                ] ===
                proposedLines[
                proposedIndex
                ]
            ) {
                matrix[
                    originalIndex
                ][proposedIndex] =
                    matrix[
                    originalIndex + 1
                    ][
                    proposedIndex + 1
                    ] + 1;
            } else {
                matrix[
                    originalIndex
                ][proposedIndex] =
                    Math.max(
                        matrix[
                        originalIndex + 1
                        ][proposedIndex],

                        matrix[
                        originalIndex
                        ][
                        proposedIndex + 1
                        ]
                    );
            }
        }
    }

    const operations:
        DiffOperation[] = [];

    let originalIndex = 0;
    let proposedIndex = 0;

    while (
        originalIndex < originalCount ||
        proposedIndex < proposedCount
    ) {
        if (
            originalIndex <
            originalCount &&
            proposedIndex <
            proposedCount &&
            originalLines[
            originalIndex
            ] ===
            proposedLines[
            proposedIndex
            ]
        ) {
            operations.push({
                kind: 'equal',
                text:
                    originalLines[
                    originalIndex
                    ],
                originalLineNumber:
                    originalIndex,
                proposedLineNumber:
                    proposedIndex
            });

            originalIndex += 1;
            proposedIndex += 1;
            continue;
        }

        const removeScore =
            originalIndex < originalCount
                ? matrix[
                originalIndex + 1
                ][proposedIndex]
                : -1;

        const addScore =
            proposedIndex < proposedCount
                ? matrix[
                originalIndex
                ][proposedIndex + 1]
                : -1;

        if (
            proposedIndex < proposedCount &&
            addScore >= removeScore
        ) {
            operations.push({
                kind: 'add',
                text:
                    proposedLines[
                    proposedIndex
                    ],
                proposedLineNumber:
                    proposedIndex
            });

            proposedIndex += 1;
            continue;
        }

        operations.push({
            kind: 'remove',
            text:
                originalLines[
                originalIndex
                ],
            originalLineNumber:
                originalIndex
        });

        originalIndex += 1;
    }

    return operations;
}

function createDiffLines(
    operations: DiffOperation[]
): DiffLine[] {
    const result: DiffLine[] = [];

    let operationIndex = 0;
    let lineId = 1;

    while (
        operationIndex <
        operations.length
    ) {
        const operation =
            operations[operationIndex];

        if (operation.kind === 'equal') {
            result.push({
                id: `line-${lineId}`,
                kind: 'unchanged',

                originalLine:
                    operation.text,

                proposedLine:
                    operation.text,

                originalLineNumber:
                    operation.originalLineNumber,

                proposedLineNumber:
                    operation.proposedLineNumber,

                decision: 'accepted'
            });

            lineId += 1;
            operationIndex += 1;
            continue;
        }

        const removed:
            DiffOperation[] = [];

        const added:
            DiffOperation[] = [];

        while (
            operationIndex <
            operations.length &&
            operations[operationIndex]
                .kind !== 'equal'
        ) {
            const changedOperation =
                operations[operationIndex];

            if (
                changedOperation.kind ===
                'remove'
            ) {
                removed.push(
                    changedOperation
                );
            } else {
                added.push(
                    changedOperation
                );
            }

            operationIndex += 1;
        }

        const alignments =
            alignChangedOperations(
                removed,
                added
            );

        for (const alignment of alignments) {
            result.push({
                id:
                    `change-line-${lineId}`,

                kind:
                    alignment.removed &&
                        alignment.added
                        ? 'modified'
                        : alignment.added
                            ? 'added'
                            : 'removed',

                originalLine:
                    alignment.removed
                        ?.text,

                proposedLine:
                    alignment.added
                        ?.text,

                originalLineNumber:
                    alignment.removed
                        ?.originalLineNumber,

                proposedLineNumber:
                    alignment.added
                        ?.proposedLineNumber,

                decision: 'pending'
            });

            lineId += 1;
        }
    }

    return result;
}

type ChangedAlignment = {
    removed?: DiffOperation;
    added?: DiffOperation;
};

function alignChangedOperations(
    removed: DiffOperation[],
    added: DiffOperation[]
): ChangedAlignment[] {
    const usedRemoved =
        new Set<number>();

    const usedAdded =
        new Set<number>();

    const pairs =
        new Map<
            number,
            ChangedAlignment
        >();

    const candidates:
        Array<{
            removedIndex: number;
            addedIndex: number;
            similarity: number;
        }> = [];

    /*
     * First pair similar non-blank lines.
     */
    for (
        let removedIndex = 0;
        removedIndex < removed.length;
        removedIndex += 1
    ) {
        for (
            let addedIndex = 0;
            addedIndex < added.length;
            addedIndex += 1
        ) {
            const originalText =
                removed[removedIndex].text;

            const proposedText =
                added[addedIndex].text;

            if (
                isBlankLine(originalText) ||
                isBlankLine(proposedText)
            ) {
                continue;
            }

            const similarity =
                calculateLineSimilarity(
                    originalText,
                    proposedText
                );

            if (similarity >= 0.3) {
                candidates.push({
                    removedIndex,
                    addedIndex,
                    similarity
                });
            }
        }
    }

    candidates.sort(
        (left, right) =>
            right.similarity -
            left.similarity
    );

    for (const candidate of candidates) {
        if (
            usedRemoved.has(
                candidate.removedIndex
            ) ||
            usedAdded.has(
                candidate.addedIndex
            )
        ) {
            continue;
        }

        usedRemoved.add(
            candidate.removedIndex
        );

        usedAdded.add(
            candidate.addedIndex
        );

        pairs.set(
            candidate.addedIndex,
            {
                removed:
                    removed[
                    candidate.removedIndex
                    ],

                added:
                    added[
                    candidate.addedIndex
                    ]
            }
        );
    }

    /*
     * Pair a remaining removed line with a remaining
     * blank proposed line only when no unmatched
     * non-blank proposed line remains.
     *
     * This represents replacing code with a blank line
     * as one modified block instead of "-" plus "+".
     */
    const unmatchedNonBlankAdded =
        added.some(
            (operation, index) =>
                !usedAdded.has(index) &&
                !isBlankLine(
                    operation.text
                )
        );

    if (!unmatchedNonBlankAdded) {
        for (
            let addedIndex = 0;
            addedIndex < added.length;
            addedIndex += 1
        ) {
            if (
                usedAdded.has(addedIndex) ||
                !isBlankLine(
                    added[addedIndex].text
                )
            ) {
                continue;
            }

            const removedIndex =
                findNearestUnusedRemoved(
                    removed,
                    usedRemoved,
                    addedIndex
                );

            if (removedIndex === undefined) {
                continue;
            }

            usedRemoved.add(removedIndex);
            usedAdded.add(addedIndex);

            pairs.set(
                addedIndex,
                {
                    removed:
                        removed[removedIndex],

                    added:
                        added[addedIndex]
                }
            );
        }
    }

    const alignments:
        ChangedAlignment[] = [];

    /*
     * Preserve proposed-file order.
     */
    for (
        let addedIndex = 0;
        addedIndex < added.length;
        addedIndex += 1
    ) {
        const paired =
            pairs.get(addedIndex);

        if (paired) {
            alignments.push(paired);
        } else {
            alignments.push({
                added: added[addedIndex]
            });
        }
    }

    /*
     * Remaining original lines are genuine deletions.
     */
    for (
        let removedIndex = 0;
        removedIndex < removed.length;
        removedIndex += 1
    ) {
        if (
            !usedRemoved.has(
                removedIndex
            )
        ) {
            alignments.push({
                removed:
                    removed[removedIndex]
            });
        }
    }

    return alignments;
}

function findNearestUnusedRemoved(
    removed: DiffOperation[],
    usedRemoved: Set<number>,
    preferredIndex: number
): number | undefined {
    let selectedIndex:
        number | undefined;

    let selectedDistance =
        Number.POSITIVE_INFINITY;

    for (
        let index = 0;
        index < removed.length;
        index += 1
    ) {
        if (usedRemoved.has(index)) {
            continue;
        }

        const distance =
            Math.abs(
                index -
                preferredIndex
            );

        if (
            distance <
            selectedDistance
        ) {
            selectedIndex = index;
            selectedDistance = distance;
        }
    }

    return selectedIndex;
}

function calculateLineSimilarity(
    originalLine: string,
    proposedLine: string
): number {
    const original =
        normalizeLineForComparison(
            originalLine
        );

    const proposed =
        normalizeLineForComparison(
            proposedLine
        );

    if (
        original.length === 0 ||
        proposed.length === 0
    ) {
        return 0;
    }

    if (original === proposed) {
        return 1;
    }

    const distance =
        calculateLevenshteinDistance(
            original,
            proposed
        );

    return (
        1 -
        distance /
        Math.max(
            original.length,
            proposed.length
        )
    );
}

function normalizeLineForComparison(
    line: string
): string {
    return line
        .trim()
        .replace(/\s+/g, ' ');
}

function isBlankLine(
    line: string
): boolean {
    return line.trim().length === 0;
}

function calculateLevenshteinDistance(
    left: string,
    right: string
): number {
    const previousRow =
        Array.from(
            {
                length:
                    right.length + 1
            },
            (_, index) => index
        );

    const currentRow =
        new Array<number>(
            right.length + 1
        );

    for (
        let leftIndex = 1;
        leftIndex <= left.length;
        leftIndex += 1
    ) {
        currentRow[0] = leftIndex;

        for (
            let rightIndex = 1;
            rightIndex <= right.length;
            rightIndex += 1
        ) {
            const substitutionCost =
                left[leftIndex - 1] ===
                    right[rightIndex - 1]
                    ? 0
                    : 1;

            currentRow[rightIndex] =
                Math.min(
                    currentRow[
                    rightIndex - 1
                    ] + 1,

                    previousRow[
                    rightIndex
                    ] + 1,

                    previousRow[
                    rightIndex - 1
                    ] +
                    substitutionCost
                );
        }

        for (
            let index = 0;
            index <= right.length;
            index += 1
        ) {
            previousRow[index] =
                currentRow[index];
        }
    }

    return previousRow[right.length];
}

function groupIntoHunks(
    lines: DiffLine[]
): DiffHunk[] {
    if (lines.length === 0) {
        return [];
    }

    const hunks: DiffHunk[] = [];

    let currentLines:
        DiffLine[] = [];

    let hunkNumber = 1;
    let index = 0;

    const flush = (): void => {
        if (
            currentLines.length === 0
        ) {
            return;
        }

        const hunk: DiffHunk = {
            id: `hunk-${hunkNumber}`,
            lines: currentLines,
            decision: 'accepted'
        };

        refreshHunkDecision(hunk);

        hunks.push(hunk);

        hunkNumber += 1;
        currentLines = [];
    };

    while (index < lines.length) {
        const line = lines[index];

        if (line.kind !== 'unchanged') {
            currentLines.push(line);
            index += 1;
            continue;
        }

        const unchangedRun:
            DiffLine[] = [];

        while (
            index < lines.length &&
            lines[index].kind ===
            'unchanged'
        ) {
            unchangedRun.push(
                lines[index]
            );

            index += 1;
        }

        const currentHasChanges =
            currentLines.some(
                (item) =>
                    item.kind !==
                    'unchanged'
            );

        const nextLine =
            lines[index];

        const nextIsChanged =
            nextLine !== undefined &&
            nextLine.kind !==
            'unchanged';

        if (
            currentHasChanges &&
            nextIsChanged &&
            unchangedRun.length <=
            maximumUnchangedLinesInsideHunk
        ) {
            currentLines.push(
                ...unchangedRun
            );

            continue;
        }

        if (currentHasChanges) {
            const trailingContext =
                unchangedRun.slice(
                    0,
                    maximumUnchangedLinesInsideHunk
                );

            currentLines.push(
                ...trailingContext
            );

            flush();

            const remainingLines =
                unchangedRun.slice(
                    maximumUnchangedLinesInsideHunk
                );

            if (
                remainingLines.length >
                maximumUnchangedLinesInsideHunk
            ) {
                const middleEnd =
                    remainingLines.length -
                    maximumUnchangedLinesInsideHunk;

                const middleLines =
                    remainingLines.slice(
                        0,
                        middleEnd
                    );

                if (
                    middleLines.length > 0
                ) {
                    currentLines.push(
                        ...middleLines
                    );

                    flush();
                }

                currentLines.push(
                    ...remainingLines.slice(
                        middleEnd
                    )
                );
            } else {
                currentLines.push(
                    ...remainingLines
                );
            }

            continue;
        }

        currentLines.push(
            ...unchangedRun
        );

        if (nextIsChanged) {
            const contextStart =
                Math.max(
                    0,
                    currentLines.length -
                    maximumUnchangedLinesInsideHunk
                );

            const earlierLines =
                currentLines.slice(
                    0,
                    contextStart
                );

            const leadingContext =
                currentLines.slice(
                    contextStart
                );

            if (
                earlierLines.length > 0
            ) {
                currentLines =
                    earlierLines;

                flush();
            }

            currentLines =
                leadingContext;
        }
    }

    flush();

    return hunks;
}

function appendReviewedLine(
    outputLines: string[],
    line: DiffLine
): void {
    if (line.kind === 'unchanged') {
        outputLines.push(
            line.proposedLine ?? ''
        );

        return;
    }

    if (
        line.decision !== 'rejected'
    ) {
        if (
            line.proposedLine !==
            undefined
        ) {
            outputLines.push(
                line.proposedLine
            );
        }

        return;
    }

    if (
        line.originalLine !==
        undefined
    ) {
        outputLines.push(
            line.originalLine
        );
    }
}

function requireHunk(
    fileDiff: FileDiff,
    hunkId: string
): DiffHunk {
    const hunk =
        fileDiff.hunks.find(
            (item) =>
                item.id === hunkId
        );

    if (!hunk) {
        throw new Error(
            `Unknown diff hunk: ${hunkId}`
        );
    }

    return hunk;
}

function hasChanges(
    hunk: DiffHunk
): boolean {
    return hunk.lines.some(
        (line) =>
            line.kind !== 'unchanged'
    );
}

function refreshHunkDecision(
    hunk: DiffHunk
): void {
    const changedLines =
        hunk.lines.filter(
            (line) =>
                line.kind !==
                'unchanged'
        );

    if (changedLines.length === 0) {
        hunk.decision = 'accepted';
        return;
    }

    if (
        changedLines.every(
            (line) =>
                line.decision ===
                'accepted'
        )
    ) {
        hunk.decision = 'accepted';
        return;
    }

    if (
        changedLines.every(
            (line) =>
                line.decision ===
                'rejected'
        )
    ) {
        hunk.decision = 'rejected';
        return;
    }

    hunk.decision = 'pending';
}
<<<END_FILE path="src/changes/diffHunks.ts">>>

<<<BEGIN_FILE path="src/changes/parser.ts" language="typescript" size="3.8 KB" sha256="7073bc6dbd776794eb02d800a3392877036004a9fe1e1082aac8bf11045ff877">>>
import type {
    ChangeItem,
    ChangeOperation
} from '../types';

type ChangeMarker = {
    kind: 'BEGIN_CHANGE' | 'END_CHANGE';
    attributes: Record<string, string>;
};

export function parseChangeBundle(
    text: string
): ChangeItem[] {
    validateChangeMarkers(text);

    const marker =
        /^<<<BEGIN_CHANGE\s+([^>]+)>>>(?:\r?\n)?([\s\S]*?)(?:\r?\n)?^<<<END_CHANGE\s+([^>]+)>>>\r?$/gm;

    const changes: ChangeItem[] = [];
    let match: RegExpExecArray | null;

    while ((match = marker.exec(text)) !== null) {
        const attributes =
            parseMarkerAttributes(match[1]);

        const operation =
            attributes.operation as ChangeOperation;

        if (
            ![
                'modify',
                'create',
                'delete',
                'rename'
            ].includes(operation)
        ) {
            throw new Error(
                `Unsupported change operation: ${
                    attributes.operation ?? '(missing)'
                }`
            );
        }

        changes.push({
            operation,
            path: attributes.path,
            newPath: attributes.new_path,
            originalSha256:
                attributes.original_sha256,
            content: match[2]
        });
    }

    if (changes.length === 0) {
        throw new Error(
            'No valid BEGIN_CHANGE / END_CHANGE blocks were found.'
        );
    }

    return changes;
}

function validateChangeMarkers(
    text: string
): void {
    const markerPattern =
        /^<<<(BEGIN_CHANGE|END_CHANGE)(?:\s+([^>]+))?>>>\r?$/gm;

    let openMarker: ChangeMarker | undefined;
    let markerCount = 0;
    let match: RegExpExecArray | null;

    while (
        (match = markerPattern.exec(text)) !== null
    ) {
        markerCount += 1;

        const marker: ChangeMarker = {
            kind: match[1] as ChangeMarker['kind'],
            attributes:
                parseMarkerAttributes(match[2] ?? '')
        };

        if (!marker.attributes.path) {
            throw new Error(
                `${marker.kind} is missing its path attribute.`
            );
        }

        if (marker.kind === 'BEGIN_CHANGE') {
            if (openMarker) {
                throw new Error(
                    `BEGIN_CHANGE for ${marker.attributes.path} was found before END_CHANGE for ${openMarker.attributes.path}.`
                );
            }

            openMarker = marker;
            continue;
        }

        if (!openMarker) {
            throw new Error(
                `END_CHANGE for ${marker.attributes.path} has no matching BEGIN_CHANGE.`
            );
        }

        if (
            openMarker.attributes.path !==
            marker.attributes.path
        ) {
            throw new Error(
                `Change marker path mismatch: BEGIN_CHANGE path is ${openMarker.attributes.path}, but END_CHANGE path is ${marker.attributes.path}.`
            );
        }

        openMarker = undefined;
    }

    if (openMarker) {
        throw new Error(
            `BEGIN_CHANGE for ${openMarker.attributes.path} has no matching END_CHANGE.`
        );
    }

    if (markerCount === 0) {
        throw new Error(
            'No valid BEGIN_CHANGE / END_CHANGE markers were found.'
        );
    }
}

function parseMarkerAttributes(
    source: string
): Record<string, string> {
    const attributes: Record<string, string> = {};

    // Attribute names may contain numbers,
    // such as original_sha256.
    const attributePattern =
        /([a-z0-9_]+)="([^"]*)"/g;

    let match: RegExpExecArray | null;

    while (
        (match = attributePattern.exec(source)) !==
        null
    ) {
        attributes[match[1]] = match[2];
    }

    return attributes;
}
<<<END_FILE path="src/changes/parser.ts">>>

<<<BEGIN_FILE path="src/changes/reviewCodeLens.ts" language="typescript" size="7.1 KB" sha256="dce88aa496c32eb357f5818c5adf7dca194fc141d2242fcc61248d12f2b869f9">>>
import * as vscode from 'vscode';

import type {
    DiffHunk,
    DiffLine
} from './diffHunks';

import {
    countChangedLines
} from './diffHunks';

import {
    PendingChangeDecorations
} from './decorations';

import {
    createReviewedLineMap,
    getReviewedLineNumber
} from './reviewedLineMap';

import type {
    ReviewedLineMap
} from './reviewedLineMap';

export const reviewCodeLensChangedEvent =
    new vscode.EventEmitter<void>();

export class ReviewCodeLensProvider
implements vscode.CodeLensProvider,
vscode.Disposable {
    public readonly onDidChangeCodeLenses =
        reviewCodeLensChangedEvent.event;

    public constructor(
        private readonly decorations:
            PendingChangeDecorations
    ) {}

    public provideCodeLenses(
        document: vscode.TextDocument
    ): vscode.CodeLens[] {
        const fileDiff =
            this.decorations.getFileDiff(
                document.uri
            );

        if (!fileDiff) {
            return [];
        }

        const reviewedLineMap =
            createReviewedLineMap(
                fileDiff
            );

        const codeLenses:
            vscode.CodeLens[] = [
                ...this.createFileActions(
                    document
                )
            ];

        for (const hunk of fileDiff.hunks) {
            if (!hasPendingChanges(hunk)) {
                continue;
            }

            codeLenses.push(
                ...this.createHunkActions(
                    document,
                    reviewedLineMap,
                    hunk
                )
            );
        }

        return codeLenses;
    }

    public refresh(): void {
        reviewCodeLensChangedEvent.fire();
    }

    public dispose(): void {
        reviewCodeLensChangedEvent.dispose();
    }

    private createFileActions(
        document: vscode.TextDocument
    ): vscode.CodeLens[] {
        const range =
            createZeroLengthRange(0);

        return [
            new vscode.CodeLens(
                range,
                {
                    title: vscode.l10n.t(
                        '$(check-all) Accept all changes in this file'
                    ),
                    command:
                        'code-bundler.acceptCurrentChange',
                    arguments: [
                        document.uri
                    ],
                    tooltip: vscode.l10n.t(
                        'Keep all AI changes in this file'
                    )
                }
            ),

            new vscode.CodeLens(
                range,
                {
                    title: vscode.l10n.t(
                        '$(discard) Reject all changes in this file'
                    ),
                    command:
                        'code-bundler.rejectCurrentChange',
                    arguments: [
                        document.uri
                    ],
                    tooltip: vscode.l10n.t(
                        'Restore this file to its original content'
                    )
                }
            )
        ];
    }

    private createHunkActions(
        document: vscode.TextDocument,
        reviewedLineMap:
            ReviewedLineMap,
        hunk: DiffHunk
    ): vscode.CodeLens[] {
        const lineNumber =
            getHunkAnchorLine(
                document,
                reviewedLineMap,
                hunk
            );

        const range =
            createZeroLengthRange(
                lineNumber
            );

        const changedLineCount =
            countChangedLines(hunk);

        return [
            new vscode.CodeLens(
                range,
                {
                    title: vscode.l10n.t(
                        '$(check) Accept this change block'
                    ),
                    command:
                        'code-bundler.acceptHunk',
                    arguments: [
                        document.uri,
                        hunk.id
                    ],
                    tooltip: vscode.l10n.t(
                        'Keep all modifications in this change block'
                    )
                }
            ),

            new vscode.CodeLens(
                range,
                {
                    title: vscode.l10n.t(
                        '$(close) Reject this change block'
                    ),
                    command:
                        'code-bundler.rejectHunk',
                    arguments: [
                        document.uri,
                        hunk.id
                    ],
                    tooltip: vscode.l10n.t(
                        'Restore the original content of this change block'
                    )
                }
            ),

            new vscode.CodeLens(
                range,
                {
                    title: vscode.l10n.t(
                        '$(diff) {0} changed lines',
                        changedLineCount
                    ),
                    command: '',
                    tooltip: vscode.l10n.t(
                        'Number of changed lines in this change block'
                    )
                }
            )
        ];
    }
}

function hasPendingChanges(
    hunk: DiffHunk
): boolean {
    return (
        hunk.decision === 'pending' &&
        hunk.lines.some(
            (line: DiffLine) =>
                line.kind !== 'unchanged' &&
                line.decision === 'pending'
        )
    );
}

function getHunkAnchorLine(
    document: vscode.TextDocument,
    reviewedLineMap: ReviewedLineMap,
    hunk: DiffHunk
): number {
    const changedLine =
        hunk.lines.find(
            (line: DiffLine) =>
                line.kind !== 'unchanged' &&
                line.decision === 'pending' &&
                isVisibleReviewedLine(line)
        ) ??
        hunk.lines.find(
            (line: DiffLine) =>
                line.kind !== 'unchanged' &&
                line.decision === 'pending'
        );

    if (!changedLine) {
        return 0;
    }

    return clampLineNumber(
        document,
        getReviewedLineNumber(
            reviewedLineMap,
            changedLine
        )
    );
}

function isVisibleReviewedLine(
    line: DiffLine
): boolean {
    if (line.kind === 'unchanged') {
        return true;
    }

    if (line.decision === 'rejected') {
        return (
            line.originalLine !== undefined
        );
    }

    return (
        line.proposedLine !== undefined
    );
}

function clampLineNumber(
    document: vscode.TextDocument,
    lineNumber: number
): number {
    return Math.max(
        0,
        Math.min(
            lineNumber,
            document.lineCount - 1
        )
    );
}

function createZeroLengthRange(
    lineNumber: number
): vscode.Range {
    const position =
        new vscode.Position(
            lineNumber,
            0
        );

    return new vscode.Range(
        position,
        position
    );
}
<<<END_FILE path="src/changes/reviewCodeLens.ts">>>

<<<BEGIN_FILE path="src/changes/reviewContentProvider.ts" language="typescript" size="1012 B" sha256="0a5f88505ab391868538c264a3e3d1d834d024883f2275fcc5ba4e5cb429469d">>>
import * as vscode from 'vscode';

export class ReviewContentProvider
implements vscode.TextDocumentContentProvider {
    public static readonly scheme =
        'code-bundler-review';

    private readonly contents =
        new Map<string, string>();

    public createUri(
        itemId: string,
        side: 'original' | 'proposed',
        filePath: string,
        content: string
    ): vscode.Uri {
        const fileName = filePath
            .replace(/\\/g, '/')
            .split('/')
            .pop() ?? 'file.txt';
        const uri = vscode.Uri.from({
            scheme: ReviewContentProvider.scheme,
            path: `/${itemId}/${side}/${fileName}`
        });

        this.contents.set(uri.toString(), content);
        return uri;
    }

    public provideTextDocumentContent(
        uri: vscode.Uri
    ): string {
        return this.contents.get(uri.toString()) ?? '';
    }

    public clear(): void {
        this.contents.clear();
    }
}

<<<END_FILE path="src/changes/reviewContentProvider.ts">>>

<<<BEGIN_FILE path="src/changes/reviewContext.ts" language="typescript" size="310 B" sha256="14a975c15717928a9a98e039298aa02508a43a04271ad7f9474fd9ddb2a56e2b">>>
import * as vscode from 'vscode';

export const reviewActiveContext =
    'codeBundler.reviewActive';

export async function setReviewActive(
    active: boolean
): Promise<void> {
    await vscode.commands.executeCommand(
        'setContext',
        reviewActiveContext,
        active
    );
}
<<<END_FILE path="src/changes/reviewContext.ts">>>

<<<BEGIN_FILE path="src/changes/reviewedLineMap.ts" language="typescript" size="1.2 KB" sha256="469c30e2f0a61f7914479f2c22ca53f7f122784f6cfd2b4dede09928c7b58543">>>
import type {
    DiffLine,
    FileDiff
} from './diffHunks';

export type ReviewedLineMap =
    Map<string, number>;

export function createReviewedLineMap(
    fileDiff: FileDiff
): ReviewedLineMap {
    const lineMap:
        ReviewedLineMap = new Map();

    let reviewedLineNumber = 0;

    for (const hunk of fileDiff.hunks) {
        for (const line of hunk.lines) {
            lineMap.set(
                line.id,
                reviewedLineNumber
            );

            if (
                isLinePresentInReviewedContent(
                    line
                )
            ) {
                reviewedLineNumber += 1;
            }
        }
    }

    return lineMap;
}

export function getReviewedLineNumber(
    lineMap: ReviewedLineMap,
    line: DiffLine
): number {
    return lineMap.get(line.id) ?? 0;
}

function isLinePresentInReviewedContent(
    line: DiffLine
): boolean {
    if (line.kind === 'unchanged') {
        return true;
    }

    if (line.decision === 'rejected') {
        return (
            line.originalLine !== undefined
        );
    }

    return (
        line.proposedLine !== undefined
    );
}
<<<END_FILE path="src/changes/reviewedLineMap.ts">>>

<<<BEGIN_FILE path="src/changes/reviewFileListView.ts" language="typescript" size="4.9 KB" sha256="2e8ffc9023bf9583fb54898aefa15aba90e189e7de0a460634a221630b3e9451">>>
import * as vscode from 'vscode';

import {
    PendingChangeDecorations
} from './decorations';

export class ReviewFileListView
implements vscode.WebviewViewProvider,
vscode.Disposable {
    public static readonly viewType =
        'codeBundler.reviewFileList';

    private view:
        vscode.WebviewView | undefined;

    private readonly disposables:
        vscode.Disposable[] = [];

    public constructor(
        private readonly decorations:
            PendingChangeDecorations
    ) {
        this.disposables.push(
            decorations.onDidChangeFileDecorations(
                () => this.refresh()
            )
        );
    }

    public resolveWebviewView(
        view: vscode.WebviewView
    ): void {
        this.view = view;
        view.webview.options = {
            enableScripts: true
        };

        this.disposables.push(
            view.webview.onDidReceiveMessage(
                async (message: {
                    command?: string;
                    uri?: string;
                }) => {
                    if (
                        message.command !== 'openFile' ||
                        !message.uri
                    ) {
                        return;
                    }

                    const document =
                        await vscode.workspace
                            .openTextDocument(
                                vscode.Uri.parse(
                                    message.uri
                                )
                            );

                    await vscode.window
                        .showTextDocument(
                            document,
                            {
                                preview: false
                            }
                        );
                }
            )
        );

        this.refresh();
    }

    public dispose(): void {
        for (const disposable of this.disposables) {
            disposable.dispose();
        }
    }

    private refresh(): void {
        if (!this.view) {
            return;
        }

        this.view.webview.html =
            this.createHtml(
                this.view.webview
            );
    }

    private createHtml(
        webview: vscode.Webview
    ): string {
        const files =
            this.decorations.getPendingUris();
        const nonce = createNonce();
        const rows = files.map((uri) => {
            const workspaceFolder =
                vscode.workspace
                    .getWorkspaceFolder(uri);
            const label = workspaceFolder
                ? vscode.workspace.asRelativePath(
                    uri,
                    false
                )
                : uri.fsPath;

            return `<button class="file" data-uri="${
                escapeHtml(uri.toString())
            }" title="${escapeHtml(label)}">${
                escapeHtml(label)
            }</button>`;
        }).join('');
        const content = rows ||
            '<div class="empty">No pending files</div>';

        return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { padding: 8px; color: var(--vscode-foreground); }
.file { display: block; width: 100%; margin: 0 0 4px; padding: 6px 8px; overflow: hidden; color: var(--vscode-foreground); background: transparent; border: 0; border-radius: 3px; text-align: left; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.file:hover { background: var(--vscode-list-hoverBackground); }
.file:focus { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; }
.empty { padding: 8px; color: var(--vscode-descriptionForeground); }
</style>
</head>
<body>
${content}
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
document.querySelectorAll('.file').forEach((button) => {
    button.addEventListener('click', () => {
        vscode.postMessage({
            command: 'openFile',
            uri: button.dataset.uri
        });
    });
});
</script>
</body>
</html>`;
    }
}

function escapeHtml(
    value: string
): string {
    return value
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
}

function createNonce(): string {
    const characters =
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let nonce = '';

    for (let index = 0; index < 32; index += 1) {
        nonce += characters.charAt(
            Math.floor(
                Math.random() * characters.length
            )
        );
    }

    return nonce;
}

<<<END_FILE path="src/changes/reviewFileListView.ts">>>

<<<BEGIN_FILE path="src/changes/validator.ts" language="typescript" size="2.8 KB" sha256="3a289b889a325aadbca122a2abf2c76dd753f19f73c7e8685f982f79ff4a1616">>>
import type { ChangeItem } from '../types';

import {
    fileExists,
    readTextFileContent,
    resolveWorkspacePath
} from '../utils/fileSystem';

import { sha256Text } from '../utils/hash';

export async function validateChangeBundle(
    changes: ChangeItem[],
    workspaceRoot: string
): Promise<void> {
    const seenPaths = new Set<string>();

    for (const change of changes) {
        const normalizedPath = change.path.replace(
            /\\/g,
            '/'
        );

        if (seenPaths.has(normalizedPath)) {
            throw new Error(
                `Duplicate change path: ${change.path}`
            );
        }

        seenPaths.add(normalizedPath);

        const fullPath = resolveWorkspacePath(
            workspaceRoot,
            change.path
        );

        const exists = await fileExists(fullPath);

        if (
            change.operation === 'create' &&
            exists
        ) {
            throw new Error(
                `Create target already exists: ${change.path}`
            );
        }

        if (
            change.operation !== 'create' &&
            !exists
        ) {
            throw new Error(
                `Change target does not exist: ${change.path}`
            );
        }

        if (change.operation === 'rename') {
            await validateRenameChange(
                change,
                workspaceRoot
            );
        }

        if (change.operation !== 'create') {
            await validateOriginalHash(
                change,
                fullPath
            );
        }
    }
}

async function validateRenameChange(
    change: ChangeItem,
    workspaceRoot: string
): Promise<void> {
    if (!change.newPath) {
        throw new Error(
            `Rename is missing new_path: ${change.path}`
        );
    }

    if (change.content.length > 0) {
        throw new Error(
            `Rename blocks must not contain file content: ${change.path}`
        );
    }

    const newFullPath = resolveWorkspacePath(
        workspaceRoot,
        change.newPath
    );

    if (await fileExists(newFullPath)) {
        throw new Error(
            `Rename target already exists: ${change.newPath}`
        );
    }
}

async function validateOriginalHash(
    change: ChangeItem,
    fullPath: string
): Promise<void> {
    if (!change.originalSha256) {
        throw new Error(
            `Missing original_sha256: ${change.path}`
        );
    }

    const currentContent =
        await readTextFileContent(fullPath);

    const currentSha256 =
        sha256Text(currentContent);

    if (
        currentSha256 !==
        change.originalSha256.toLowerCase()
    ) {
        throw new Error(
            `File changed since bundling: ${change.path}`
        );
    }
}
<<<END_FILE path="src/changes/validator.ts">>>

<<<BEGIN_FILE path="src/commands/applyChangeBundleCommand.ts" language="typescript" size="5.7 KB" sha256="1401f26ddebc115fc8b119119092dc4752606586b3cdb313c61040393c5e5f03">>>
import * as vscode from 'vscode';

import {
    parseChangeBundle
} from '../changes/parser';

import {
    validateChangeBundle
} from '../changes/validator';

import {
    AppliedReviewManager
} from '../changes/appliedReviewManager';

import type {
    ChangeItem
} from '../types';

import {
    decodeTextBufferBestEffort
} from '../utils/encoding';

import {
    readTextFileContent,
    resolveWorkspacePath
} from '../utils/fileSystem';

export function registerApplyChangeBundleCommand(
    reviewManager: AppliedReviewManager
): vscode.Disposable {
    return vscode.commands.registerCommand(
        'code-bundler.applyChangeBundle',
        async (uri?: vscode.Uri) => {
            try {
                const changeBundleUri =
                    await selectChangeBundle(
                        uri
                    );

                if (!changeBundleUri) {
                    return;
                }

                if (
                    reviewManager
                        .hasActiveSession
                ) {
                    vscode.window
                        .showWarningMessage(
                            vscode.l10n.t(
                                'A change review is already active.'
                            )
                        );

                    return;
                }

                const workspaceRoot =
                    getWorkspaceRoot();

                if (!workspaceRoot) {
                    throw new Error(
                        vscode.l10n.t(
                            'Please open a workspace folder first.'
                        )
                    );
                }

                const changeBundleText =
                    await readChangeBundle(
                        changeBundleUri
                    );

                const changes =
                    parseChangeBundle(
                        changeBundleText
                    );

                await validateChangeBundle(
                    changes,
                    workspaceRoot
                );

                if (
                    !await hasEffectiveChanges(
                        changes,
                        workspaceRoot
                    )
                ) {
                    vscode.window
                        .showInformationMessage(
                            vscode.l10n.t(
                                'The change bundle matches the current project. There are no changes to review.'
                            )
                        );

                    return;
                }

                await reviewManager.start(
                    changes,
                    workspaceRoot
                );

                if (
                    reviewManager
                        .hasActiveSession
                ) {
                    vscode.window
                        .showInformationMessage(
                            vscode.l10n.t(
                                'AI changes were applied. You can now run the project, then accept or reject each change.'
                            )
                        );
                }
            } catch (error) {
                const message =
                    error instanceof Error
                        ? error.message
                        : String(error);

                vscode.window
                    .showErrorMessage(
                        vscode.l10n.t(
                            'Unable to apply change bundle: {0}',
                            message
                        )
                    );
            }
        }
    );
}

async function hasEffectiveChanges(
    changes: ChangeItem[],
    workspaceRoot: string
): Promise<boolean> {
    for (const change of changes) {
        if (change.operation !== 'modify') {
            return true;
        }

        const currentContent =
            await readTextFileContent(
                resolveWorkspacePath(
                    workspaceRoot,
                    change.path
                )
            );

        if (
            normalizeLineEndings(
                currentContent
            ) !==
            normalizeLineEndings(
                change.content
            )
        ) {
            return true;
        }
    }

    return false;
}

async function selectChangeBundle(
    uri?: vscode.Uri
): Promise<vscode.Uri | undefined> {
    if (
        uri &&
        uri.scheme === 'file' &&
        uri.fsPath
            .toLowerCase()
            .endsWith('.txt')
    ) {
        return uri;
    }

    const pickedUris =
        await vscode.window.showOpenDialog({
            canSelectFiles: true,
            canSelectFolders: false,
            canSelectMany: false,

            filters: {
                [vscode.l10n.t(
                    'Code Bundler change bundle'
                )]: [
                    'txt'
                ]
            },

            title: vscode.l10n.t(
                'Select an AI change bundle'
            )
        });

    return pickedUris?.[0];
}

function getWorkspaceRoot():
    string | undefined {
    return vscode.workspace
        .workspaceFolders?.[0]
        ?.uri.fsPath;
}

async function readChangeBundle(
    uri: vscode.Uri
): Promise<string> {
    const bytes =
        await vscode.workspace.fs
            .readFile(uri);

    return decodeTextBufferBestEffort(
        Buffer.from(bytes)
    );
}

function normalizeLineEndings(
    content: string
): string {
    return content.replace(
        /\r\n/g,
        '\n'
    );
}
<<<END_FILE path="src/commands/applyChangeBundleCommand.ts">>>

<<<BEGIN_FILE path="src/commands/packCommand.ts" language="typescript" size="13.1 KB" sha256="4db6cb0d5f4e8c399fcc9c2e16f6f3a74084e3dee18011de589ba83b9e173cb9">>>
import * as vscode from 'vscode';
import * as fs from 'fs/promises';
import * as path from 'path';
import { createHash } from 'crypto';

import type {
    PackedFile,
    PackResult
} from '../types';

import {
    bundleFilePrefix,
    createBundleFileName
} from '../constants';
import {
    collectFromUris,
    getInputUris,
    getWorkspaceRoot
} from '../bundle/collector';
import { buildAiFriendlyBundle } from '../bundle/builder';
import { formatBytes } from '../utils/format';
import {
    tryCopyGeneratedFileToClipboard
} from '../utils/clipboard';

export function registerPackCommand(
    outputChannel: vscode.OutputChannel
): vscode.Disposable {
    return vscode.commands.registerCommand(
        'code-bundler.packSelected',
        async (
            uri?: vscode.Uri,
            selectedUris?: vscode.Uri[]
        ) => {
            try {
                const inputUris =
                    await getInputUris(
                        uri,
                        selectedUris
                    );

                if (inputUris.length === 0) {
                    vscode.window.showWarningMessage(
                        vscode.l10n.t(
                            'Please select one or more files or folders first.'
                        )
                    );

                    return;
                }

                const workspaceRoot =
                    getWorkspaceRoot(
                        inputUris[0]
                    );

                if (!workspaceRoot) {
                    vscode.window.showWarningMessage(
                        vscode.l10n.t(
                            'Please open a workspace folder first.'
                        )
                    );

                    return;
                }

                const packResult =
                    await runPackOperation(
                        inputUris,
                        workspaceRoot
                    );

                if (!packResult.success) {
                    showFailedResult(
                        packResult,
                        outputChannel
                    );

                    return;
                }

                showSuccessfulResult(
                    packResult,
                    outputChannel
                );

                await offerToOpenOutputFile(
                    packResult
                );
            } catch (error) {
                const message =
                    error instanceof Error
                        ? error.message
                        : String(error);

                vscode.window.showErrorMessage(
                    vscode.l10n.t(
                        'Bundle failed: {0}',
                        message
                    )
                );
            }
        }
    );
}

async function runPackOperation(
    inputUris: vscode.Uri[],
    workspaceRoot: string
): Promise<PackResult> {
    return vscode.window.withProgress(
        {
            location:
                vscode.ProgressLocation.Notification,
            title: vscode.l10n.t(
                'Code Bundler is bundling files...'
            ),
            cancellable: false
        },
        async (progress): Promise<PackResult> => {
            progress.report({
                message: vscode.l10n.t(
                    'Scanning selected files and folders...'
                )
            });

            const collectResult =
                await collectFromUris(
                    inputUris,
                    workspaceRoot
                );

            if (collectResult.totalFiles === 0) {
                return {
                    success: false,
                    message: vscode.l10n.t(
                        'No regular files were scanned.'
                    )
                };
            }

            if (
                collectResult.packedFiles.length === 0
            ) {
                return {
                    success: false,
                    message: vscode.l10n.t(
                        'Scanned {0} files, but no text files could be packed.',
                        collectResult.totalFiles
                    )
                };
            }

            progress.report({
                message: vscode.l10n.t(
                    'Scanned {0} files, generating AI-friendly format...',
                    collectResult.totalFiles
                )
            });

            const bundleId =
                await createBundleId(
                    collectResult.packedFiles
                );
            const outputFileName =
                createBundleFileName(bundleId);

            const bundledText =
                await buildAiFriendlyBundle(
                    collectResult,
                    workspaceRoot,
                    bundleId
                );

            const outputPath = path.join(
                workspaceRoot,
                outputFileName
            );

            const outputSizeBytes =
                Buffer.byteLength(
                    bundledText,
                    'utf8'
                );

            progress.report({
                message: vscode.l10n.t(
                    'Writing {0}...',
                    outputFileName
                )
            });

            await fs.writeFile(
                outputPath,
                bundledText,
                'utf8'
            );

            await deletePreviousBundleFiles(
                workspaceRoot,
                outputFileName
            );

            progress.report({
                message: vscode.l10n.t(
                    'Copying {0} file to clipboard...',
                    outputFileName
                )
            });

            const clipboardResult =
                await tryCopyGeneratedFileToClipboard(
                    outputPath
                );

            const message =
                createCompletionMessage(
                    collectResult.totalFiles,
                    collectResult.packedFiles.length,
                    collectResult.skippedFiles.length,
                    collectResult.skippedDirs.length,
                    outputSizeBytes,
                    clipboardResult
                );

            return {
                success: true,
                message,
                outputPath,
                totalFiles:
                    collectResult.totalFiles,
                packedFiles:
                    collectResult.packedFiles.length,
                skippedFiles:
                    collectResult.skippedFiles.length,
                skippedDirs:
                    collectResult.skippedDirs.length,
                outputSizeBytes,
                clipboardCopied:
                    clipboardResult.copied,
                clipboardMessage:
                    clipboardResult.message
            };
        }
    );
}

async function deletePreviousBundleFiles(
    workspaceRoot: string,
    currentBundleFileName: string
): Promise<void> {
    const directoryEntries =
        await fs.readdir(
            workspaceRoot,
            {
                withFileTypes: true
            }
        );

    const normalizedCurrentFileName =
        currentBundleFileName.toLowerCase();

    await Promise.all(
        directoryEntries
            .filter(
                (entry) =>
                    entry.isFile() &&
                    entry.name
                        .toLowerCase()
                        .startsWith(bundleFilePrefix) &&
                    entry.name
                        .toLowerCase()
                        .endsWith('.txt') &&
                    entry.name.toLowerCase() !==
                        normalizedCurrentFileName
            )
            .map(
                (entry) =>
                    fs.unlink(
                        path.join(
                            workspaceRoot,
                            entry.name
                        )
                    )
            )
    );
}

function createCompletionMessage(
    totalFiles: number,
    packedFiles: number,
    skippedFiles: number,
    skippedDirs: number,
    outputSizeBytes: number,
    clipboardResult: {
        copied: boolean;
        message: string;
    }
): string {
    if (clipboardResult.copied) {
        return vscode.l10n.t(
            'Bundle completed.\nTotal scanned files: {0}\nPacked files: {1}\nSkipped files: {2}\nSkipped directories: {3}\nOutput size: {4}\nThe generated file has been copied to the clipboard and can be pasted into another folder.',
            totalFiles,
            packedFiles,
            skippedFiles,
            skippedDirs,
            formatBytes(outputSizeBytes)
        );
    }

    return vscode.l10n.t(
        'Bundle completed.\nTotal scanned files: {0}\nPacked files: {1}\nSkipped files: {2}\nSkipped directories: {3}\nOutput size: {4}\nThe file was generated, but copying it to the clipboard was not completed: {5}',
        totalFiles,
        packedFiles,
        skippedFiles,
        skippedDirs,
        formatBytes(outputSizeBytes),
        clipboardResult.message
    );
}

function showFailedResult(
    result: Extract<
        PackResult,
        { success: false }
    >,
    outputChannel: vscode.OutputChannel
): void {
    outputChannel.clear();

    outputChannel.appendLine(
        vscode.l10n.t(
            'Code Bundler did not complete bundling.'
        )
    );

    outputChannel.appendLine(
        '----------------------------------------'
    );

    outputChannel.appendLine(
        vscode.l10n.t(
            'Reason: {0}',
            result.message
        )
    );

    outputChannel.appendLine(
        '----------------------------------------'
    );

    outputChannel.show(true);

    vscode.window.showWarningMessage(
        result.message
    );
}

function showSuccessfulResult(
    result: Extract<
        PackResult,
        { success: true }
    >,
    outputChannel: vscode.OutputChannel
): void {
    outputChannel.clear();

    outputChannel.appendLine(
        vscode.l10n.t(
            'Code Bundler completed bundling.'
        )
    );

    outputChannel.appendLine(
        '----------------------------------------'
    );

    outputChannel.appendLine(
        vscode.l10n.t(
            'Total scanned files: {0}',
            result.totalFiles
        )
    );

    outputChannel.appendLine(
        vscode.l10n.t(
            'Packed files: {0}',
            result.packedFiles
        )
    );

    outputChannel.appendLine(
        vscode.l10n.t(
            'Skipped files: {0}',
            result.skippedFiles
        )
    );

    outputChannel.appendLine(
        vscode.l10n.t(
            'Skipped directories: {0}',
            result.skippedDirs
        )
    );

    outputChannel.appendLine(
        vscode.l10n.t(
            'Output file: {0}',
            result.outputPath
        )
    );

    outputChannel.appendLine(
        vscode.l10n.t(
            'Output size: {0}',
            formatBytes(
                result.outputSizeBytes
            )
        )
    );

    outputChannel.appendLine(
        vscode.l10n.t(
            'Clipboard status: {0}',
            result.clipboardCopied
                ? vscode.l10n.t(
                    'the generated txt file has been copied to the system clipboard.'
                )
                : result.clipboardMessage
        )
    );

    if (result.clipboardCopied) {
        outputChannel.appendLine(
            vscode.l10n.t(
                'Tip: you can now paste the file into another folder with Ctrl+V.'
            )
        );
    }

    outputChannel.appendLine(
        '----------------------------------------'
    );

    outputChannel.show(true);
}

async function offerToOpenOutputFile(
    result: Extract<
        PackResult,
        { success: true }
    >
): Promise<void> {
    const openFileLabel =
        vscode.l10n.t('Open File');

    const selectedAction =
        await vscode.window.showInformationMessage(
            result.message,
            openFileLabel
        );

    if (selectedAction !== openFileLabel) {
        return;
    }

    const document =
        await vscode.workspace.openTextDocument(
            result.outputPath
        );

    await vscode.window.showTextDocument(
        document
    );
}

async function createBundleId(
    files: PackedFile[]
): Promise<string> {
    const hash = createHash('sha256');
    const orderedFiles = [...files].sort(
        (fileA, fileB) =>
            fileA.relativePath.localeCompare(
                fileB.relativePath
            )
    );

    for (const file of orderedFiles) {
        hash.update(file.relativePath, 'utf8');
        hash.update('\0', 'utf8');
        hash.update(await fs.readFile(file.fullPath));
        hash.update('\0', 'utf8');
    }

    return hash.digest('hex').slice(0, 16);
}
<<<END_FILE path="src/commands/packCommand.ts">>>

<<<BEGIN_FILE path="src/constants.ts" language="typescript" size="2.0 KB" sha256="be0f428c358209add98fbfd479863df6c47cd68096467a3bef036d82ae3b67f8">>>
export const bundleFilePrefix = 'code-bundle-';
export const changeFilePrefix = 'code-change-';

export function createBundleFileName(
    bundleId: string
): string {
    return `${bundleFilePrefix}${bundleId}.txt`;
}

export function createChangeFileName(
    bundleId: string
): string {
    return `${changeFilePrefix}${bundleId}.txt`;
}

export function isGeneratedExchangeFile(
    fileName: string
): boolean {
    const normalized = fileName.toLowerCase();

    return (
        normalized.startsWith(bundleFilePrefix) ||
        normalized.startsWith(changeFilePrefix)
    ) && normalized.endsWith('.txt');
}

// Skip a single file if it is larger than this size.
export const maxFileSizeBytes = 500 * 1024 * 1024;

export const ignoredDirs = new Set([
    'node_modules',
    '.git',
    'dist',
    'out',
    'build',
    'coverage',
    '.vscode-test',
    '.next',
    '.nuxt',
    '.turbo',
    '.cache',
    'target'
]);

export const ignoredFiles = new Set([
    'code-bundle.txt'
]);

export const obviousBinaryExtensions = new Set([
    // Images
    '.png',
    '.jpg',
    '.jpeg',
    '.gif',
    '.webp',
    '.ico',
    '.bmp',
    '.tiff',
    '.psd',
    '.ai',

    // Audio and video
    '.mp3',
    '.wav',
    '.flac',
    '.aac',
    '.ogg',
    '.mp4',
    '.mov',
    '.avi',
    '.mkv',
    '.webm',

    // Archives
    '.zip',
    '.rar',
    '.7z',
    '.tar',
    '.gz',
    '.bz2',
    '.xz',
    '.iso',

    // Binaries and libraries
    '.exe',
    '.dll',
    '.so',
    '.dylib',
    '.bin',
    '.obj',
    '.o',
    '.class',
    '.jar',
    '.war',
    '.wasm',

    // Office and binary documents
    '.pdf',
    '.doc',
    '.docx',
    '.xls',
    '.xlsx',
    '.ppt',
    '.pptx',

    // Fonts
    '.ttf',
    '.otf',
    '.woff',
    '.woff2',
    '.eot',

    // Databases
    '.db',
    '.sqlite',
    '.sqlite3',
    '.mdb'
]);
<<<END_FILE path="src/constants.ts">>>

<<<BEGIN_FILE path="src/test/extension.test.ts" language="typescript" size="474 B" sha256="6286d2b97121e5fbe38fafa5636d7563a781c1bc5fb14c363d8fc901c78a0de3">>>
import * as assert from 'assert';

// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';

suite('Extension Test Suite', () => {
	vscode.window.showInformationMessage('Start all tests.');

	test('Sample test', () => {
		assert.strictEqual(-1, [1, 2, 3].indexOf(5));
		assert.strictEqual(-1, [1, 2, 3].indexOf(0));
	});
});

<<<END_FILE path="src/test/extension.test.ts">>>

<<<BEGIN_FILE path="src/types.ts" language="typescript" size="1.1 KB" sha256="bc93046fe874d462110c8e08340295ef1c54a1d1fe64b0ea72aa028fb8a3c655">>>
export type PackedFile = {
    fullPath: string;
    relativePath: string;
    sizeBytes: number;
};

export type SkippedItem = {
    relativePath: string;
    reason: string;
};

export type CollectResult = {
    packedFiles: PackedFile[];
    skippedFiles: SkippedItem[];
    skippedDirs: SkippedItem[];
    totalFiles: number;
};

export type PackResult =
    | {
        success: true;
        message: string;
        outputPath: string;
        totalFiles: number;
        packedFiles: number;
        skippedFiles: number;
        skippedDirs: number;
        outputSizeBytes: number;
        clipboardCopied: boolean;
        clipboardMessage: string;
    }
    | {
        success: false;
        message: string;
    };

export type FileTreeNode = {
    directories: Map<string, FileTreeNode>;
    files: string[];
};

export type ChangeOperation =
    | 'modify'
    | 'create'
    | 'delete'
    | 'rename';

export type ChangeItem = {
    operation: ChangeOperation;
    path: string;
    newPath?: string;
    originalSha256?: string;
    content: string;
};
<<<END_FILE path="src/types.ts">>>

<<<BEGIN_FILE path="src/utils/clipboard.ts" language="typescript" size="2.3 KB" sha256="eacd1b8fc0e4a0defce8180bced86e931cb78a2cfa713b104ce85ee7353b5a28">>>
import * as vscode from 'vscode';
import { execFile } from 'child_process';
import { promisify } from 'util';

const execFileAsync = promisify(execFile);

export async function tryCopyGeneratedFileToClipboard(
    filePath: string
): Promise<{
    copied: boolean;
    message: string;
}> {
    try {
        await copyGeneratedFileToClipboard(filePath);

        return {
            copied: true,
            message: vscode.l10n.t('Copied to clipboard.')
        };
    } catch (error) {
        return {
            copied: false,
            message:
                error instanceof Error
                    ? error.message
                    : String(error)
        };
    }
}

async function copyGeneratedFileToClipboard(
    filePath: string
): Promise<void> {
    if (process.platform === 'win32') {
        await copyFileToClipboardOnWindows(filePath);
        return;
    }

    if (process.platform === 'darwin') {
        await copyFileToClipboardOnMac(filePath);
        return;
    }

    throw new Error(
        vscode.l10n.t(
            'The current platform does not support copying file objects to the clipboard.'
        )
    );
}

async function copyFileToClipboardOnWindows(
    filePath: string
): Promise<void> {
    const escapedPath =
        escapePowerShellSingleQuotedString(filePath);

    const script = `
Add-Type -AssemblyName System.Windows.Forms
$files = New-Object System.Collections.Specialized.StringCollection
[void]$files.Add('${escapedPath}')
[System.Windows.Forms.Clipboard]::SetFileDropList($files)
`;

    await execFileAsync(
        'powershell.exe',
        [
            '-NoProfile',
            '-STA',
            '-ExecutionPolicy',
            'Bypass',
            '-Command',
            script
        ],
        {
            windowsHide: true
        }
    );
}

async function copyFileToClipboardOnMac(
    filePath: string
): Promise<void> {
    const escapedPath = filePath
        .replace(/\\/g, '\\\\')
        .replace(/"/g, '\\"');

    await execFileAsync('osascript', [
        '-e',
        `set the clipboard to POSIX file "${escapedPath}"`
    ]);
}

function escapePowerShellSingleQuotedString(
    value: string
): string {
    return value.replace(/'/g, "''");
}
<<<END_FILE path="src/utils/clipboard.ts">>>

<<<BEGIN_FILE path="src/utils/encoding.ts" language="typescript" size="4.0 KB" sha256="b1b659a6c4194af696eb13a5ec15e4fd3bb29f2ffe3efcdf3f483982d7a3b828">>>
export function hasTextBom(buffer: Buffer): boolean {
    if (buffer.length >= 3) {
        const isUtf8Bom =
            buffer[0] === 0xef &&
            buffer[1] === 0xbb &&
            buffer[2] === 0xbf;

        if (isUtf8Bom) {
            return true;
        }
    }

    if (buffer.length >= 2) {
        const isUtf16LeBom =
            buffer[0] === 0xff &&
            buffer[1] === 0xfe;

        const isUtf16BeBom =
            buffer[0] === 0xfe &&
            buffer[1] === 0xff;

        if (isUtf16LeBom || isUtf16BeBom) {
            return true;
        }
    }

    return false;
}

export function looksLikeUtf16Text(buffer: Buffer): boolean {
    if (buffer.length < 4) {
        return false;
    }

    let evenNulls = 0;
    let oddNulls = 0;
    const length = Math.min(buffer.length, 2000);

    for (let index = 0; index < length; index++) {
        if (buffer[index] === 0) {
            if (index % 2 === 0) {
                evenNulls += 1;
            } else {
                oddNulls += 1;
            }
        }
    }

    const halfLength = Math.floor(length / 2);

    if (halfLength === 0) {
        return false;
    }

    return (
        evenNulls / halfLength > 0.3 ||
        oddNulls / halfLength > 0.3
    );
}

export function containsNullByte(buffer: Buffer): boolean {
    for (const byte of buffer) {
        if (byte === 0) {
            return true;
        }
    }

    return false;
}

export function hasTooManyControlBytes(buffer: Buffer): boolean {
    let controlBytes = 0;

    for (const byte of buffer) {
        const isAllowedWhitespace =
            byte === 9 ||
            byte === 10 ||
            byte === 12 ||
            byte === 13;

        if (byte < 32 && !isAllowedWhitespace) {
            controlBytes += 1;
        }
    }

    return controlBytes / buffer.length > 0.3;
}

export function decodeTextBufferBestEffort(
    buffer: Buffer
): string {
    if (
        buffer.length >= 3 &&
        buffer[0] === 0xef &&
        buffer[1] === 0xbb &&
        buffer[2] === 0xbf
    ) {
        return buffer.subarray(3).toString('utf8');
    }

    if (
        buffer.length >= 2 &&
        buffer[0] === 0xff &&
        buffer[1] === 0xfe
    ) {
        return buffer.subarray(2).toString('utf16le');
    }

    if (
        buffer.length >= 2 &&
        buffer[0] === 0xfe &&
        buffer[1] === 0xff
    ) {
        return swapUtf16ByteOrder(
            buffer.subarray(2)
        ).toString('utf16le');
    }

    if (looksLikeUtf16LeBuffer(buffer)) {
        return buffer.toString('utf16le');
    }

    if (looksLikeUtf16BeBuffer(buffer)) {
        return swapUtf16ByteOrder(buffer).toString('utf16le');
    }

    return buffer.toString('utf8');
}

export function looksLikeUtf16LeBuffer(
    buffer: Buffer
): boolean {
    if (buffer.length < 4) {
        return false;
    }

    let oddNulls = 0;
    const length = Math.min(buffer.length, 2000);

    for (let index = 1; index < length; index += 2) {
        if (buffer[index] === 0) {
            oddNulls += 1;
        }
    }

    return oddNulls / Math.floor(length / 2) > 0.3;
}

export function looksLikeUtf16BeBuffer(
    buffer: Buffer
): boolean {
    if (buffer.length < 4) {
        return false;
    }

    let evenNulls = 0;
    const length = Math.min(buffer.length, 2000);

    for (let index = 0; index < length; index += 2) {
        if (buffer[index] === 0) {
            evenNulls += 1;
        }
    }

    return evenNulls / Math.floor(length / 2) > 0.3;
}

export function swapUtf16ByteOrder(
    buffer: Buffer
): Buffer {
    const swapped = Buffer.from(buffer);

    for (
        let index = 0;
        index + 1 < swapped.length;
        index += 2
    ) {
        const first = swapped[index];
        swapped[index] = swapped[index + 1];
        swapped[index + 1] = first;
    }

    return swapped;
}
<<<END_FILE path="src/utils/encoding.ts">>>

<<<BEGIN_FILE path="src/utils/fileSystem.ts" language="typescript" size="2.0 KB" sha256="48a8601b369c5248f88714a8ae53466cabd7bb18402c7f014e2ca49a31d39889">>>
import * as vscode from 'vscode';
import * as fs from 'fs/promises';
import * as path from 'path';
import { decodeTextBufferBestEffort } from './encoding';

export async function readTextFileContent(
    filePath: string
): Promise<string> {
    try {
        const document = await vscode.workspace.openTextDocument(
            vscode.Uri.file(filePath)
        );

        return document.getText();
    } catch {
        try {
            const buffer = await fs.readFile(filePath);
            return decodeTextBufferBestEffort(buffer);
        } catch {
            return '[Failed to read: unable to read this file as text]';
        }
    }
}

export async function fileExists(
    filePath: string
): Promise<boolean> {
    try {
        await fs.stat(filePath);
        return true;
    } catch {
        return false;
    }
}

export function toRelativePath(
    filePath: string,
    workspaceRoot: string
): string {
    return path
        .relative(workspaceRoot, filePath)
        .replace(/\\/g, '/');
}

export function resolveWorkspacePath(
    workspaceRoot: string,
    relativePath: string
): string {
    const normalized = relativePath.replace(/\\/g, '/');

    if (
        !normalized ||
        path.isAbsolute(normalized) ||
        normalized.split('/').includes('..')
    ) {
        throw new Error(
            `Unsafe workspace path: ${relativePath}`
        );
    }

    const resolved = path.resolve(
        workspaceRoot,
        normalized
    );

    const relative = path.relative(
        workspaceRoot,
        resolved
    );

    if (
        relative.startsWith('..') ||
        path.isAbsolute(relative)
    ) {
        throw new Error(
            `Path escapes the workspace: ${relativePath}`
        );
    }

    return resolved;
}

export function escapeMarkerAttribute(
    value: string
): string {
    return value
        .replace(/&/g, '&amp;')
        .replace(/"/g, '&quot;')
        .replace(/\r/g, ' ')
        .replace(/\n/g, ' ');
}
<<<END_FILE path="src/utils/fileSystem.ts">>>

<<<BEGIN_FILE path="src/utils/format.ts" language="typescript" size="265 B" sha256="87c71cd8ef160c1bc602c5d7cd6d2a6cc7a35603e81a48edaf3e149693fcb655">>>
export function formatBytes(bytes: number): string {
    if (bytes < 1024) {
        return `${bytes} B`;
    }

    if (bytes < 1024 * 1024) {
        return `${(bytes / 1024).toFixed(1)} KB`;
    }

    return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
<<<END_FILE path="src/utils/format.ts">>>

<<<BEGIN_FILE path="src/utils/hash.ts" language="typescript" size="188 B" sha256="86121bb45560178ddfc4005cd62044ef6e909cc22221541d05333f31587e54bc">>>
import { createHash } from 'crypto';

export function sha256Text(content: string): string {
    return createHash('sha256')
        .update(content, 'utf8')
        .digest('hex');
}
<<<END_FILE path="src/utils/hash.ts">>>

---

## 7. Skipped Items

This section lists files and directories that were scanned or encountered but not included in the complete file contents.

Skipped items are provided for transparency only. They are not part of the source content available for analysis.

### 7.1 Skipped Files

```text
(none)
```

Skipped file line format:

```text
relative/path/to/file.ext - reason
```

### 7.2 Skipped Directories

```text
(none)
```

Skipped directory line format:

```text
relative/path/to/directory - reason
```

---

## 8. End Of Bundle

This is the end of the complete-file source bundle.

If a file is not listed in the Packed File Index or Complete File Contents section, assume it was not included in this bundle.
