Compare commits

..

3 commits

Author SHA1 Message Date
Anish Lakhwara f75c054e65 update 2025-11-08 21:58:34 -08:00
Anish Lakhwara 35983681e4 Update diff viewer 2025-11-08 21:15:14 -08:00
Anish Lakhwara e04c67133b diff viewer 2025-11-08 00:33:44 -08:00
14 changed files with 1634 additions and 5 deletions

20
.beads/.gitignore vendored Normal file
View file

@ -0,0 +1,20 @@
# SQLite databases
*.db
*.db-journal
*.db-wal
*.db-shm
# Daemon runtime files
daemon.lock
daemon.log
daemon.pid
bd.sock
# Legacy database files
db.sqlite
bd.db
# Keep JSONL exports and config (source of truth for git)
!*.jsonl
!metadata.json
!config.json

56
.beads/config.yaml Normal file
View file

@ -0,0 +1,56 @@
# Beads Configuration File
# This file configures default behavior for all bd commands in this repository
# All settings can also be set via environment variables (BD_* prefix)
# or overridden with command-line flags
# Issue prefix for this repository (used by bd init)
# If not set, bd init will auto-detect from directory name
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
# issue-prefix: ""
# Use no-db mode: load from JSONL, no SQLite, write back after each command
# When true, bd will use .beads/issues.jsonl as the source of truth
# instead of SQLite database
# no-db: false
# Disable daemon for RPC communication (forces direct database access)
# no-daemon: false
# Disable auto-flush of database to JSONL after mutations
# no-auto-flush: false
# Disable auto-import from JSONL when it's newer than database
# no-auto-import: false
# Enable JSON output by default
# json: false
# Default actor for audit trails (overridden by BD_ACTOR or --actor)
# actor: ""
# Path to database (overridden by BEADS_DB or --db)
# db: ""
# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON)
# auto-start-daemon: true
# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE)
# flush-debounce: "5s"
# Multi-repo configuration (experimental - bd-307)
# Allows hydrating from multiple repositories and routing writes to the correct JSONL
# repos:
# primary: "." # Primary repo (where this database lives)
# additional: # Additional repos to hydrate from (read-only)
# - ~/beads-planning # Personal planning repo
# - ~/work-planning # Work planning repo
# Integration settings (access with 'bd config get/set')
# These are stored in the database, not in this file:
# - jira.url
# - jira.project
# - linear.url
# - linear.api-key
# - github.org
# - github.repo
# - sync.branch - Git branch for beads commits (use BEADS_SYNC_BRANCH env var or bd config set)

5
.beads/metadata.json Normal file
View file

@ -0,0 +1,5 @@
{
"database": "beads.db",
"version": "0.23.0",
"jsonl_export": "beads.jsonl"
}

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
# Use bd merge for beads JSONL files
.beads/beads.jsonl merge=beads

View file

@ -0,0 +1,95 @@
# Git Diff Viewer Implementation
## Overview
The Git Diff Viewer feature has been successfully implemented for FleetCode's web mode. This feature allows users to view changes in their session's worktree compared to the parent branch.
## Implementation Details
### 1. Dependencies
- **@pierre/precision-diffs**: A powerful diff rendering library
- **shiki**: Syntax highlighting for code diffs
### 2. Data Structure (types.ts)
Added `FileDiff` interface to represent file changes:
```typescript
export interface FileDiff {
path: string;
oldPath?: string;
status: 'added' | 'modified' | 'deleted' | 'renamed';
diff: string;
additions: number;
deletions: number;
}
```
### 3. Backend (server.ts)
- Added Socket.IO handler `get-session-diff` that:
- Retrieves the session's worktree path and parent branch
- Executes `git diff <parent-branch>...HEAD` using simple-git
- Parses the diff output into `FileDiff` objects
- Emits the parsed data back to the client via `session-diff-data` event
### 4. Frontend UI (index.html)
- Added full-screen diff viewer modal with:
- Header with title and close button
- Left sidebar for file list
- Right panel for displaying diffs
- Added "Diff" button to active session items in the sidebar
### 5. Frontend Styling (styles.css)
- Added comprehensive styling for:
- Diff viewer modal (full-screen, responsive)
- File list sidebar with status badges
- Diff display area
- File status indicators (added/modified/deleted/renamed)
- Addition/deletion statistics
- Mobile-responsive layouts
### 6. Frontend Logic (renderer.ts)
- Added diff viewer functionality:
- `openDiffViewer(sessionId)`: Opens modal and requests diff data
- `renderDiffFileList()`: Renders list of changed files
- `renderSelectedDiff()`: Renders the selected file's diff using Pierre.js
- Dynamic import of libraries to avoid breaking Electron mode
- Event handlers for Socket.IO events (`session-diff-data`, `session-diff-error`)
## Usage
1. Start a session and make some changes in your worktree
2. Click the "Diff" button next to an active session in the sidebar
3. A modal will appear showing all changed files
4. Click on any file to view its detailed diff
5. Click the × button or outside the modal to close
## Technical Notes
- Libraries (@pierre/precision-diffs and shiki) are loaded dynamically using `import()` to avoid conflicts with Electron's module system
- The diff is computed between the parent branch and HEAD of the worktree
- TypeScript configuration was updated to use ES2020 modules with bundler resolution for better ESM support
- The implementation is fully functional in web mode (server.ts) and does not affect Electron mode (main.ts)
## Build & Run
1. **Build the application:**
```bash
npm run build:all
```
2. **Start the server:**
```bash
npm run server
# or for development with hot reload:
npm run dev:server
```
3. **Access the application:**
Open http://localhost:3000 in your browser
## Verification
The implementation has been verified:
- ✅ Dependencies installed: `@pierre/precision-diffs` and `shiki`
- ✅ Server builds and runs successfully
- ✅ Diff handler is present in compiled server.js
- ✅ Diff libraries are bundled in renderer.web.js
- ✅ All TypeScript compiles without errors

View file

@ -1,5 +1,27 @@
{ {
"nodes": { "nodes": {
"beads": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1762602968,
"narHash": "sha256-3F7sv4mAKBQ4YBSABgp9bendgHopObNivO5NQhJYUek=",
"owner": "steveyegge",
"repo": "beads",
"rev": "367bf077e10695976480bba097e2ae47593a4d82",
"type": "github"
},
"original": {
"owner": "steveyegge",
"ref": "v0.23.0",
"repo": "beads",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
@ -18,6 +40,24 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1760038930, "lastModified": 1760038930,
@ -36,7 +76,8 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "beads": "beads",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
}, },
@ -54,6 +95,21 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View file

@ -4,9 +4,13 @@
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
beads = {
url = "github:steveyegge/beads/v0.23.0";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { self, nixpkgs, flake-utils }: outputs = { self, nixpkgs, flake-utils, beads }:
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
@ -25,6 +29,9 @@
(python3.withPackages (ps: with ps; [ setuptools ])) (python3.withPackages (ps: with ps; [ setuptools ]))
pkg-config pkg-config
# Beads - AI code review tool
beads.packages.${system}.default
# Native build dependencies for node-pty and Electron # Native build dependencies for node-pty and Electron
gcc gcc
gnumake gnumake

View file

@ -196,6 +196,37 @@
</div> </div>
</div> </div>
<!-- Diff Viewer Modal -->
<div id="diff-viewer-modal" class="modal-overlay hidden">
<div class="diff-viewer-modal">
<div class="diff-viewer-header">
<h2 class="modal-title" id="diff-viewer-title">Git Diff Viewer</h2>
<div class="diff-viewer-actions">
<button id="merge-to-parent-btn" class="btn-merge" disabled title="Merge worktree branch into parent">
Merge to Parent
</button>
<button id="close-diff-viewer" class="diff-viewer-close" title="Close">×</button>
</div>
</div>
<div class="diff-viewer-content">
<!-- File List Sidebar -->
<div class="diff-file-list" id="diff-file-list">
<button id="toggle-diff-sidebar" class="diff-sidebar-toggle">
<span class="toggle-expand">&gt;&gt;</span>
<span class="toggle-collapse">&lt;&lt;</span>
</button>
<div class="diff-loading">Loading diff...</div>
</div>
<!-- Diff Display -->
<div class="diff-display" id="diff-display">
<div class="diff-empty">Select a file to view changes</div>
</div>
</div>
</div>
</div>
<!-- Settings Modal --> <!-- Settings Modal -->
<div id="settings-modal" class="modal-overlay hidden"> <div id="settings-modal" class="modal-overlay hidden">
<div class="modal"> <div class="modal">

591
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "1.0.1-beta.7", "version": "1.0.1-beta.7",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@pierre/precision-diffs": "^0.4.2",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
@ -16,6 +17,7 @@
"electron-store": "^11.0.0", "electron-store": "^11.0.0",
"express": "^5.1.0", "express": "^5.1.0",
"node-pty": "^1.0.0", "node-pty": "^1.0.0",
"shiki": "^3.14.0",
"simple-git": "^3.28.0", "simple-git": "^3.28.0",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
@ -1926,6 +1928,33 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0" "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
} }
}, },
"node_modules/@pierre/precision-diffs": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@pierre/precision-diffs/-/precision-diffs-0.4.2.tgz",
"integrity": "sha512-C6LbruH24BCp4awI47D5iMtVaZZD6GkzqBoDw+Sfu7DB3hC9y/rZr1C2BD7AUzAKwByTfFnh16Zp11ipfPqLKw==",
"license": "apache-2.0",
"dependencies": {
"@shikijs/core": "3.14.0",
"@shikijs/transformers": "3.14.0",
"diff": "8.0.2",
"fast-deep-equal": "3.1.3",
"hast-util-to-html": "9.0.5",
"shiki": "3.14.0"
},
"peerDependencies": {
"react": "^18.3.1 || ^19.0.0",
"react-dom": "^18.3.1 || ^19.0.0"
}
},
"node_modules/@pierre/precision-diffs/node_modules/diff": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -1937,6 +1966,83 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@shikijs/core": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.14.0.tgz",
"integrity": "sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.14.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.5"
}
},
"node_modules/@shikijs/engine-javascript": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.14.0.tgz",
"integrity": "sha512-3v1kAXI2TsWQuwv86cREH/+FK9Pjw3dorVEykzQDhwrZj0lwsHYlfyARaKmn6vr5Gasf8aeVpb8JkzeWspxOLQ==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.14.0",
"@shikijs/vscode-textmate": "^10.0.2",
"oniguruma-to-es": "^4.3.3"
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.14.0.tgz",
"integrity": "sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.14.0",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
"node_modules/@shikijs/langs": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.14.0.tgz",
"integrity": "sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.14.0"
}
},
"node_modules/@shikijs/themes": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.14.0.tgz",
"integrity": "sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.14.0"
}
},
"node_modules/@shikijs/transformers": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.14.0.tgz",
"integrity": "sha512-i67zQnY9wLMMnKasonVW1L9fKneSLZDj1ePsA4o0AZWU4uUobmJY9baRDa36z+a9/g0aG76/2tybQvm4hrwxIQ==",
"license": "MIT",
"dependencies": {
"@shikijs/core": "3.14.0",
"@shikijs/types": "3.14.0"
}
},
"node_modules/@shikijs/types": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.14.0.tgz",
"integrity": "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==",
"license": "MIT",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/@shikijs/vscode-textmate": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
"node_modules/@sindresorhus/is": { "node_modules/@sindresorhus/is": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@ -2091,6 +2197,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/http-cache-semantics": { "node_modules/@types/http-cache-semantics": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
@ -2114,6 +2229,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -2200,6 +2324,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/uuid": { "node_modules/@types/uuid": {
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
@ -2226,6 +2356,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@xmldom/xmldom": { "node_modules/@xmldom/xmldom": {
"version": "0.8.11", "version": "0.8.11",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
@ -3258,6 +3394,16 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/ccount": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -3275,6 +3421,26 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/character-entities-html4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-legacy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -3481,6 +3647,16 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -3934,6 +4110,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz",
@ -3952,6 +4137,19 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -5496,6 +5694,42 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/hast-util-to-html": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
"integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"ccount": "^2.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-whitespace": "^3.0.0",
"html-void-elements": "^3.0.0",
"mdast-util-to-hast": "^13.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0",
"stringify-entities": "^4.0.0",
"zwitch": "^2.0.4"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hosted-git-info": { "node_modules/hosted-git-info": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@ -5522,6 +5756,16 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/html-void-elements": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/http-cache-semantics": { "node_modules/http-cache-semantics": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@ -6143,6 +6387,27 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/mdast-util-to-hast": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"@ungap/structured-clone": "^1.0.0",
"devlop": "^1.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"trim-lines": "^3.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@ -6174,6 +6439,95 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/micromark-util-character": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-util-encode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-sanitize-uri": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-encode": "^2.0.0",
"micromark-util-symbol": "^2.0.0"
}
},
"node_modules/micromark-util-symbol": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-types": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@ -6787,6 +7141,23 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/oniguruma-parser": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
"license": "MIT"
},
"node_modules/oniguruma-to-es": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz",
"integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==",
"license": "MIT",
"dependencies": {
"oniguruma-parser": "^0.12.1",
"regex": "^6.0.1",
"regex-recursion": "^6.0.2"
}
},
"node_modules/ora": { "node_modules/ora": {
"version": "5.4.1", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
@ -7251,6 +7622,16 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -7381,6 +7762,29 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/react": {
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.0"
}
},
"node_modules/read-binary-file-arch": { "node_modules/read-binary-file-arch": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
@ -7445,6 +7849,30 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
"integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==",
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-recursion": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-utilities": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
"license": "MIT"
},
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -7687,6 +8115,13 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT",
"peer": true
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@ -7829,6 +8264,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/shiki": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.14.0.tgz",
"integrity": "sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g==",
"license": "MIT",
"dependencies": {
"@shikijs/core": "3.14.0",
"@shikijs/engine-javascript": "3.14.0",
"@shikijs/engine-oniguruma": "3.14.0",
"@shikijs/langs": "3.14.0",
"@shikijs/themes": "3.14.0",
"@shikijs/types": "3.14.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/side-channel": { "node_modules/side-channel": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@ -8183,6 +8634,16 @@
"source-map": "^0.6.0" "source-map": "^0.6.0"
} }
}, },
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/sprintf-js": { "node_modules/sprintf-js": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
@ -8264,6 +8725,20 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
"license": "MIT",
"dependencies": {
"character-entities-html4": "^2.0.0",
"character-entities-legacy": "^3.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/strip-ansi": { "node_modules/strip-ansi": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@ -8715,6 +9190,16 @@
"tree-kill": "cli.js" "tree-kill": "cli.js"
} }
}, },
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/truncate-utf8-bytes": { "node_modules/truncate-utf8-bytes": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@ -8897,6 +9382,74 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0" "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
} }
}, },
"node_modules/unist-util-is": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
"integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-position": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
"integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit-parents": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
"integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/universalify": { "node_modules/universalify": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@ -9016,6 +9569,34 @@
"node": ">=0.6.0" "node": ">=0.6.0"
} }
}, },
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
"integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-stringify-position": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/wcwidth": { "node_modules/wcwidth": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@ -9226,6 +9807,16 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
} }
} }
} }

View file

@ -44,6 +44,7 @@
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"dependencies": { "dependencies": {
"@pierre/precision-diffs": "^0.4.2",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
@ -51,6 +52,7 @@
"electron-store": "^11.0.0", "electron-store": "^11.0.0",
"express": "^5.1.0", "express": "^5.1.0",
"node-pty": "^1.0.0", "node-pty": "^1.0.0",
"shiki": "^3.14.0",
"simple-git": "^3.28.0", "simple-git": "^3.28.0",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",

View file

@ -1,6 +1,6 @@
import {FitAddon} from "@xterm/addon-fit"; import {FitAddon} from "@xterm/addon-fit";
import {Terminal} from "xterm"; import {Terminal} from "xterm";
import {PersistedSession, SessionConfig} from "./types"; import {PersistedSession, SessionConfig, FileDiff, MergeStatus, MergeResult} from "./types";
import {isClaudeSessionReady} from "./terminal-utils"; import {isClaudeSessionReady} from "./terminal-utils";
import {ipcAdapter} from "./ipc-adapter"; import {ipcAdapter} from "./ipc-adapter";
@ -340,12 +340,33 @@ function activateSession(sessionId: string) {
function updateSessionState(sessionId: string, isActive: boolean) { function updateSessionState(sessionId: string, isActive: boolean) {
const sidebarItem = document.getElementById(`sidebar-${sessionId}`); const sidebarItem = document.getElementById(`sidebar-${sessionId}`);
const indicator = sidebarItem?.querySelector(".session-indicator"); const indicator = sidebarItem?.querySelector(".session-indicator");
const buttonContainer = sidebarItem?.querySelector(".flex.items-center.space-x-1");
if (indicator) { if (indicator) {
if (isActive) { if (isActive) {
indicator.classList.add("active"); indicator.classList.add("active");
// Add diff button if it doesn't exist
if (buttonContainer && !buttonContainer.querySelector(".view-diff-btn")) {
const diffBtn = document.createElement("button");
diffBtn.className = "view-diff-btn";
diffBtn.setAttribute("data-id", sessionId);
diffBtn.setAttribute("title", "View Diff");
diffBtn.textContent = "Diff";
diffBtn.addEventListener("click", (e) => {
e.stopPropagation();
openDiffViewer(sessionId);
});
buttonContainer.insertBefore(diffBtn, buttonContainer.firstChild);
}
} else { } else {
indicator.classList.remove("active"); indicator.classList.remove("active");
// Remove diff button when session becomes inactive
const existingDiffBtn = buttonContainer?.querySelector(".view-diff-btn");
if (existingDiffBtn) {
existingDiffBtn.remove();
}
} }
} }
} }
@ -363,7 +384,10 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
<span class="truncate session-name-text" data-id="${sessionId}">${name}</span> <span class="truncate session-name-text" data-id="${sessionId}">${name}</span>
<input type="text" class="session-name-input hidden" data-id="${sessionId}" value="${name}" /> <input type="text" class="session-name-input hidden" data-id="${sessionId}" value="${name}" />
</div> </div>
<div class="flex items-center space-x-1">
${hasActivePty ? `<button class="view-diff-btn" data-id="${sessionId}" title="View Diff">Diff</button>` : ''}
<button class="session-delete-btn" data-id="${sessionId}" title="Delete session">×</button> <button class="session-delete-btn" data-id="${sessionId}" title="Delete session">×</button>
</div>
`; `;
// Click on session name to edit // Click on session name to edit
@ -391,6 +415,7 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
item.addEventListener("click", (e) => { item.addEventListener("click", (e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (!target.classList.contains("session-delete-btn") && if (!target.classList.contains("session-delete-btn") &&
!target.classList.contains("view-diff-btn") &&
!target.classList.contains("session-name-text") && !target.classList.contains("session-name-text") &&
!target.classList.contains("session-name-input")) { !target.classList.contains("session-name-input")) {
handleSessionClick(sessionId); handleSessionClick(sessionId);
@ -403,6 +428,12 @@ function addToSidebar(sessionId: string, name: string, hasActivePty: boolean) {
deleteSession(sessionId); deleteSession(sessionId);
}); });
const viewDiffBtn = item.querySelector(".view-diff-btn");
viewDiffBtn?.addEventListener("click", (e) => {
e.stopPropagation();
openDiffViewer(sessionId);
});
list.appendChild(item); list.appendChild(item);
} }
@ -1487,3 +1518,310 @@ document.getElementById("session-list")?.addEventListener("click", (e) => {
closeMobileMenu(); closeMobileMenu();
} }
}); });
// Diff Viewer
let currentDiffFiles: FileDiff[] = [];
let selectedFileIndex: number = -1;
let currentDiffSessionId: string = '';
let diffSidebarCollapsed: boolean = false;
let pierreLib: any = null;
let shikiLib: any = null;
let highlighter: any = null;
async function openDiffViewer(sessionId: string) {
const modal = document.getElementById("diff-viewer-modal");
const fileList = document.getElementById("diff-file-list");
const diffDisplay = document.getElementById("diff-display");
const mergeBtn = document.getElementById("merge-to-parent-btn") as HTMLButtonElement;
if (!modal || !fileList || !diffDisplay) return;
currentDiffSessionId = sessionId;
// Auto-collapse sidebar on mobile
const isMobile = window.innerWidth < 768;
diffSidebarCollapsed = isMobile;
if (diffSidebarCollapsed) {
fileList.classList.add("collapsed");
} else {
fileList.classList.remove("collapsed");
}
// Show modal with loading state
modal.classList.remove("hidden");
fileList.innerHTML = '<div class="diff-loading">Loading diff...</div>';
diffDisplay.innerHTML = '<div class="diff-empty">Loading...</div>';
if (mergeBtn) {
mergeBtn.disabled = true;
mergeBtn.textContent = "Merge to Parent";
}
// Load libraries dynamically (only in browser)
if (!pierreLib) {
try {
pierreLib = await import("@pierre/precision-diffs" as any);
shikiLib = await import("shiki" as any);
} catch (error) {
console.error("Failed to load diff libraries:", error);
}
}
// Initialize highlighter if needed
if (!highlighter && shikiLib) {
try {
highlighter = await shikiLib.createHighlighter({
themes: ['github-dark'],
langs: ['javascript', 'typescript', 'python', 'java', 'go', 'rust', 'html', 'css', 'json', 'markdown']
});
} catch (error) {
console.error("Failed to initialize highlighter:", error);
}
}
// Request diff and merge status from server
ipcAdapter.send("get-session-diff", sessionId);
ipcAdapter.send("check-merge-status", sessionId);
}
function renderDiffFileList() {
const fileList = document.getElementById("diff-file-list");
if (!fileList) return;
if (currentDiffFiles.length === 0) {
fileList.innerHTML = '<button id="toggle-diff-sidebar" class="diff-sidebar-toggle"><span class="toggle-expand">&gt;&gt;</span><span class="toggle-collapse">&lt;&lt;</span></button><div class="diff-loading">No changes found</div>';
return;
}
fileList.innerHTML = '<button id="toggle-diff-sidebar" class="diff-sidebar-toggle"><span class="toggle-expand">&gt;&gt;</span><span class="toggle-collapse">&lt;&lt;</span></button>';
currentDiffFiles.forEach((file, index) => {
const item = document.createElement("div");
item.className = `diff-file-item ${index === selectedFileIndex ? 'active' : ''}`;
// Create file path div and set text content securely
const pathDiv = document.createElement("div");
pathDiv.className = "diff-file-path";
pathDiv.textContent = file.path;
// Create stats div
const statsDiv = document.createElement("div");
statsDiv.className = "diff-file-stats";
// Create status badge
const statusSpan = document.createElement("span");
statusSpan.className = `diff-file-status ${file.status}`;
statusSpan.textContent = file.status;
statsDiv.appendChild(statusSpan);
// Add additions count if present
if (file.additions > 0) {
const additionsSpan = document.createElement("span");
additionsSpan.className = "diff-file-additions";
additionsSpan.textContent = `+${file.additions}`;
statsDiv.appendChild(additionsSpan);
}
// Add deletions count if present
if (file.deletions > 0) {
const deletionsSpan = document.createElement("span");
deletionsSpan.className = "diff-file-deletions";
deletionsSpan.textContent = `-${file.deletions}`;
statsDiv.appendChild(deletionsSpan);
}
item.appendChild(pathDiv);
item.appendChild(statsDiv);
item.addEventListener("click", () => {
selectedFileIndex = index;
// Auto-collapse sidebar on mobile when selecting a file
const isMobile = window.innerWidth < 768;
if (isMobile) {
const fileListEl = document.getElementById("diff-file-list");
if (fileListEl) {
diffSidebarCollapsed = true;
fileListEl.classList.add("collapsed");
}
}
renderDiffFileList();
renderSelectedDiff();
});
fileList.appendChild(item);
});
// Re-attach toggle button event listener
document.getElementById("toggle-diff-sidebar")?.replaceWith(document.getElementById("toggle-diff-sidebar")!.cloneNode(true));
document.getElementById("toggle-diff-sidebar")?.addEventListener("click", () => {
const fileListEl = document.getElementById("diff-file-list");
if (!fileListEl) return;
diffSidebarCollapsed = !diffSidebarCollapsed;
if (diffSidebarCollapsed) {
fileListEl.classList.add("collapsed");
} else {
fileListEl.classList.remove("collapsed");
}
});
}
async function renderSelectedDiff() {
const diffDisplay = document.getElementById("diff-display");
if (!diffDisplay || selectedFileIndex < 0 || selectedFileIndex >= currentDiffFiles.length) {
if (diffDisplay) {
diffDisplay.innerHTML = '<div class="diff-empty">Select a file to view changes</div>';
}
return;
}
const file = currentDiffFiles[selectedFileIndex];
try {
// Check if file is binary
if (file.binary) {
diffDisplay.innerHTML = '<div class="diff-empty">Binary file changed</div>';
return;
}
if (!pierreLib) {
diffDisplay.innerHTML = '<div class="diff-empty">Loading diff viewer...</div>';
return;
}
// Use Pierre.js FileDiff to render the diff
const diffContainer = document.createElement("div");
diffContainer.className = "diff-content";
// Create FileDiff instance
const fileDiff = new pierreLib.FileDiff({
theme: { dark: 'github-dark', light: 'github-light' },
themeType: 'dark',
});
// Parse the diff to get FileDiffMetadata
const parsedPatches = pierreLib.parsePatchFiles(file.diff);
if (!parsedPatches || parsedPatches.length === 0 || !parsedPatches[0].files || parsedPatches[0].files.length === 0) {
diffDisplay.innerHTML = '<div class="diff-empty">Could not parse diff data</div>';
return;
}
// Get the first file's diff metadata from the parsed patch
const fileDiffMetadata = parsedPatches[0].files[0];
// Render using the parsed fileDiff metadata
await fileDiff.render({
fileDiff: fileDiffMetadata,
containerWrapper: diffContainer,
});
diffDisplay.innerHTML = '';
diffDisplay.appendChild(diffContainer);
} catch (error) {
console.error("Error rendering diff:", error);
diffDisplay.innerHTML = `<div class="diff-empty">Error rendering diff: ${error}</div>`;
}
}
// Close diff viewer
document.getElementById("close-diff-viewer")?.addEventListener("click", () => {
const modal = document.getElementById("diff-viewer-modal");
modal?.classList.add("hidden");
currentDiffFiles = [];
selectedFileIndex = -1;
currentDiffSessionId = '';
diffSidebarCollapsed = false;
});
// Merge to parent button
document.getElementById("merge-to-parent-btn")?.addEventListener("click", () => {
if (!currentDiffSessionId) return;
const mergeBtn = document.getElementById("merge-to-parent-btn") as HTMLButtonElement;
if (!mergeBtn || mergeBtn.disabled) return;
if (!confirm("Merge this worktree branch into the parent branch? This cannot be undone.")) {
return;
}
mergeBtn.disabled = true;
mergeBtn.textContent = "Merging...";
ipcAdapter.send("merge-to-parent", currentDiffSessionId);
});
// Handle diff data from server
ipcAdapter.on("session-diff-data", (_event, sessionId: string, fileDiffs: FileDiff[]) => {
currentDiffFiles = fileDiffs;
selectedFileIndex = fileDiffs.length > 0 ? 0 : -1;
renderDiffFileList();
renderSelectedDiff();
});
// Handle diff error from server
ipcAdapter.on("session-diff-error", (_event, sessionId: string, error: string) => {
const fileList = document.getElementById("diff-file-list");
const diffDisplay = document.getElementById("diff-display");
if (fileList) {
fileList.innerHTML = `<div class="diff-loading">Error: ${error}</div>`;
}
if (diffDisplay) {
diffDisplay.innerHTML = `<div class="diff-empty">Failed to load diff</div>`;
}
});
// Handle merge status from server
ipcAdapter.on("merge-status-data", (_event, sessionId: string, status: MergeStatus) => {
const mergeBtn = document.getElementById("merge-to-parent-btn") as HTMLButtonElement;
if (!mergeBtn) return;
if (status.hasCommits) {
mergeBtn.disabled = false;
const commitText = status.commitsAhead === 1 ? '1 commit' : `${status.commitsAhead} commits`;
mergeBtn.title = `Merge ${commitText} into parent branch`;
if (status.hasUncommittedChanges) {
mergeBtn.title += ' (has uncommitted changes)';
}
} else {
mergeBtn.disabled = true;
mergeBtn.title = "No commits to merge";
}
});
// Handle merge status error
ipcAdapter.on("merge-status-error", (_event, sessionId: string, error: string) => {
console.error("Merge status error:", error);
const mergeBtn = document.getElementById("merge-to-parent-btn") as HTMLButtonElement;
if (mergeBtn) {
mergeBtn.disabled = true;
mergeBtn.title = `Error checking merge status: ${error}`;
}
});
// Handle merge result
ipcAdapter.on("merge-result", (_event, sessionId: string, result: MergeResult) => {
const mergeBtn = document.getElementById("merge-to-parent-btn") as HTMLButtonElement;
if (result.success) {
alert(`${result.message}`);
if (mergeBtn) {
mergeBtn.disabled = true;
mergeBtn.textContent = "Merged";
}
// Refresh diff and status
ipcAdapter.send("get-session-diff", sessionId);
ipcAdapter.send("check-merge-status", sessionId);
} else {
alert(`✗ Merge failed: ${result.message}`);
if (mergeBtn) {
mergeBtn.disabled = false;
mergeBtn.textContent = "Merge to Parent";
}
}
});

238
server.ts
View file

@ -8,7 +8,7 @@ import * as path from "path";
import { simpleGit } from "simple-git"; import { simpleGit } from "simple-git";
import { promisify } from "util"; import { promisify } from "util";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { PersistedSession, SessionConfig } from "./types"; import { PersistedSession, SessionConfig, FileDiff, MergeStatus, MergeResult } from "./types";
import { isTerminalReady } from "./terminal-utils"; import { isTerminalReady } from "./terminal-utils";
import cors from "cors"; import cors from "cors";
@ -234,6 +234,74 @@ async function removeGitBranch(projectDir: string, branchName: string) {
} }
} }
function parsePatchToFileDiffs(patch: string): FileDiff[] {
const fileDiffs: FileDiff[] = [];
// Split patch into individual file chunks
// Each chunk starts with "diff --git"
const fileChunks = patch.split(/(?=diff --git )/g).filter(chunk => chunk.trim());
for (const chunk of fileChunks) {
const lines = chunk.split('\n');
// Extract file paths from "diff --git a/... b/..."
const diffGitMatch = lines[0].match(/^diff --git a\/(.+?) b\/(.+?)$/);
if (!diffGitMatch) continue;
let oldPath = diffGitMatch[1];
let newPath = diffGitMatch[2];
// Determine status and paths
let status: FileDiff['status'] = 'modified';
let finalPath = newPath;
let finalOldPath: string | undefined;
let isBinary = false;
// Check for file mode changes and status indicators
for (const line of lines) {
if (line.startsWith('new file mode')) {
status = 'added';
finalOldPath = undefined;
} else if (line.startsWith('deleted file mode')) {
status = 'deleted';
finalPath = oldPath;
finalOldPath = undefined;
} else if (line.startsWith('rename from')) {
status = 'renamed';
finalOldPath = line.substring('rename from '.length).trim();
} else if (line.startsWith('rename to')) {
finalPath = line.substring('rename to '.length).trim();
} else if (line.startsWith('Binary files')) {
isBinary = true;
}
}
// Count additions and deletions
let additions = 0;
let deletions = 0;
for (const line of lines) {
if (line.startsWith('+') && !line.startsWith('+++')) {
additions++;
} else if (line.startsWith('-') && !line.startsWith('---')) {
deletions++;
}
}
fileDiffs.push({
path: finalPath,
oldPath: finalOldPath,
status,
diff: chunk,
additions,
deletions,
binary: isBinary,
});
}
return fileDiffs;
}
// Socket.IO handlers // Socket.IO handlers
io.on("connection", (socket) => { io.on("connection", (socket) => {
console.log("Client connected:", socket.id); console.log("Client connected:", socket.id);
@ -495,6 +563,174 @@ io.on("connection", (socket) => {
} }
}); });
socket.on("get-session-diff", async (sessionId: string) => {
try {
const sessions = getPersistedSessions();
const session = sessions.find(s => s.id === sessionId);
if (!session) {
socket.emit("session-diff-error", sessionId, "Session not found");
return;
}
const git = simpleGit(session.worktreePath);
const parentBranch = session.config.parentBranch;
console.log(`[${sessionId}] Generating unified diff for session, parent branch: ${parentBranch}`);
console.log(`[${sessionId}] Worktree path: ${session.worktreePath}`);
// Get unified patch for tracked changes (includes staged, unstaged, and committed)
const trackedPatch = await git.raw([
'diff',
parentBranch,
'--patch',
'--find-renames',
'--binary'
]);
// Get untracked files
const statusSummary = await git.status();
// Generate patches for untracked files using --no-index
const untrackedPatches: string[] = [];
for (const untrackedFile of statusSummary.not_added) {
try {
const untrackedPatch = await git.raw([
'diff',
'--no-index',
'--binary',
'/dev/null',
untrackedFile
]);
untrackedPatches.push(untrackedPatch);
} catch (error: any) {
// git diff --no-index exits with code 1 when there are differences, which is expected
if (error.message && typeof error.message === 'string') {
untrackedPatches.push(error.message);
} else {
console.error(`[${sessionId}] Error generating patch for untracked file ${untrackedFile}:`, error);
}
}
}
// Combine all patches
const combinedPatch = [trackedPatch, ...untrackedPatches].filter(p => p).join('\n');
console.log(`[${sessionId}] Combined patch length: ${combinedPatch.length} chars`);
console.log(`[${sessionId}] Untracked files: ${statusSummary.not_added.length}`);
// Parse the combined patch into FileDiff array
const fileDiffs: FileDiff[] = parsePatchToFileDiffs(combinedPatch);
console.log(`[${sessionId}] Parsed ${fileDiffs.length} file diffs`);
socket.emit("session-diff-data", sessionId, fileDiffs);
} catch (error) {
console.error("Error getting session diff:", error);
socket.emit("session-diff-error", sessionId, error instanceof Error ? error.message : String(error));
}
});
socket.on("check-merge-status", async (sessionId: string) => {
try {
const sessions = getPersistedSessions();
const session = sessions.find(s => s.id === sessionId);
if (!session) {
socket.emit("merge-status-error", sessionId, "Session not found");
return;
}
const git = simpleGit(session.worktreePath);
const parentBranch = session.config.parentBranch;
const currentBranch = session.gitBranch || (await git.revparse(['--abbrev-ref', 'HEAD']));
// Check how many commits ahead of parent
const revListOutput = await git.raw([
'rev-list',
'--count',
`${parentBranch}..${currentBranch}`
]);
const commitsAhead = parseInt(revListOutput.trim(), 10);
// Check for uncommitted changes
const statusSummary = await git.status();
const hasUncommittedChanges = !statusSummary.isClean();
const mergeStatus: MergeStatus = {
hasCommits: commitsAhead > 0,
commitsAhead,
hasUncommittedChanges
};
console.log(`[${sessionId}] Merge status: ${JSON.stringify(mergeStatus)}`);
socket.emit("merge-status-data", sessionId, mergeStatus);
} catch (error) {
console.error("Error checking merge status:", error);
socket.emit("merge-status-error", sessionId, error instanceof Error ? error.message : String(error));
}
});
socket.on("merge-to-parent", async (sessionId: string) => {
try {
const sessions = getPersistedSessions();
const session = sessions.find(s => s.id === sessionId);
if (!session) {
socket.emit("merge-result", sessionId, {
success: false,
message: "Session not found"
} as MergeResult);
return;
}
const projectDir = session.config.projectDir;
const parentBranch = session.config.parentBranch;
const worktreeBranch = session.gitBranch || (await simpleGit(session.worktreePath).revparse(['--abbrev-ref', 'HEAD']));
console.log(`[${sessionId}] Starting merge: ${worktreeBranch} -> ${parentBranch} in ${projectDir}`);
const git = simpleGit(projectDir);
// Checkout parent branch
await git.checkout(parentBranch);
console.log(`[${sessionId}] Checked out ${parentBranch}`);
// Attempt merge
try {
await git.merge([worktreeBranch]);
console.log(`[${sessionId}] Merge successful`);
socket.emit("merge-result", sessionId, {
success: true,
message: `Successfully merged ${worktreeBranch} into ${parentBranch}`
} as MergeResult);
} catch (mergeError: any) {
// Check if it's a merge conflict
const status = await git.status();
if (status.conflicted.length > 0) {
console.log(`[${sessionId}] Merge conflicts detected: ${status.conflicted.join(', ')}`);
// Abort the merge
await git.raw(['merge', '--abort']);
socket.emit("merge-result", sessionId, {
success: false,
conflicts: status.conflicted,
message: `Merge conflicts in: ${status.conflicted.join(', ')}`
} as MergeResult);
} else {
throw mergeError;
}
}
} catch (error) {
console.error("Error merging to parent:", error);
socket.emit("merge-result", sessionId, {
success: false,
message: error instanceof Error ? error.message : String(error)
} as MergeResult);
}
});
socket.on("disconnect", () => { socket.on("disconnect", () => {
// Clean up socket references for this client // Clean up socket references for this client
// Note: We don't kill the PTYs - they keep running for reconnection // Note: We don't kill the PTYs - they keep running for reconnection

View file

@ -336,4 +336,171 @@
@apply text-lg; @apply text-lg;
} }
} }
/* Diff Viewer Styles */
.diff-viewer-modal {
@apply bg-gray-900 rounded-lg shadow-xl flex flex-col;
width: 95vw;
height: 90vh;
max-width: 1400px;
}
.diff-viewer-header {
@apply flex items-center justify-between p-4 border-b border-gray-700;
}
.diff-viewer-actions {
@apply flex items-center gap-3;
}
.btn-merge {
@apply px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded font-medium transition-colors;
}
.btn-merge:disabled {
@apply bg-gray-700 text-gray-500 cursor-not-allowed;
}
.diff-viewer-close {
@apply text-gray-400 hover:text-white text-3xl font-bold w-10 h-10 flex items-center justify-center rounded transition-colors hover:bg-gray-800;
}
.diff-viewer-content {
@apply flex flex-1 overflow-hidden;
}
.diff-file-list {
@apply w-80 border-r border-gray-700 overflow-y-auto bg-gray-800 transition-all duration-300 relative;
}
.diff-file-list.collapsed {
@apply w-12 overflow-hidden;
}
.diff-loading {
@apply p-4 text-gray-400 text-center;
}
.diff-file-item {
@apply px-4 py-3 border-b border-gray-700 cursor-pointer hover:bg-gray-700 transition-colors;
}
.diff-file-item.active {
@apply bg-gray-700;
}
.diff-file-path {
@apply text-sm text-gray-200 mb-1 break-all;
}
.diff-file-stats {
@apply flex items-center space-x-2 text-xs;
}
.diff-file-status {
@apply px-2 py-0.5 rounded text-xs font-medium;
}
.diff-file-status.added {
@apply bg-green-900 text-green-300;
}
.diff-file-status.modified {
@apply bg-blue-900 text-blue-300;
}
.diff-file-status.deleted {
@apply bg-red-900 text-red-300;
}
.diff-file-status.renamed {
@apply bg-purple-900 text-purple-300;
}
.diff-file-additions {
@apply text-green-400;
}
.diff-file-deletions {
@apply text-red-400;
}
.diff-sidebar-toggle {
@apply sticky top-0 left-0 w-full bg-gray-700 hover:bg-gray-600 text-gray-300 py-2 px-3 text-sm font-mono transition-colors z-10 border-b border-gray-600 flex items-center justify-center;
}
.diff-file-list.collapsed .diff-sidebar-toggle {
@apply px-0;
}
.toggle-collapse {
@apply inline-block;
}
.diff-file-list.collapsed .toggle-collapse {
@apply hidden;
}
.toggle-expand {
@apply hidden;
}
.diff-file-list.collapsed .toggle-expand {
@apply inline-block;
}
@media (min-width: 768px) {
.diff-sidebar-toggle {
@apply hidden;
}
.diff-file-list.collapsed {
@apply w-80;
}
}
.diff-display {
@apply flex-1 overflow-auto bg-gray-900 p-4 relative;
}
.diff-empty {
@apply text-gray-500 text-center p-8;
}
.diff-content {
@apply text-sm;
}
/* Pierre.js diff styles */
.diff-content .pierre-diff {
@apply font-mono text-xs;
}
.diff-content .pierre-diff .line-number {
@apply text-gray-500 select-none;
}
.diff-content .pierre-diff .line-addition {
@apply bg-green-900 bg-opacity-30;
}
.diff-content .pierre-diff .line-deletion {
@apply bg-red-900 bg-opacity-30;
}
.view-diff-btn {
@apply ml-2 text-xs bg-blue-600 hover:bg-blue-700 text-white px-2 py-1 rounded transition-colors;
}
@media (max-width: 768px) {
.diff-viewer-modal {
width: 100vw;
height: 100vh;
@apply rounded-none;
}
.diff-file-list {
@apply w-64;
}
}
} }

View file

@ -18,3 +18,25 @@ export interface PersistedSession {
mcpConfigPath?: string; mcpConfigPath?: string;
gitBranch?: string; gitBranch?: string;
} }
export interface FileDiff {
path: string;
oldPath?: string;
status: 'added' | 'modified' | 'deleted' | 'renamed';
diff: string;
additions: number;
deletions: number;
binary?: boolean;
}
export interface MergeStatus {
hasCommits: boolean;
commitsAhead: number;
hasUncommittedChanges: boolean;
}
export interface MergeResult {
success: boolean;
conflicts?: string[];
message: string;
}