update
This commit is contained in:
parent
35983681e4
commit
f75c054e65
20
.beads/.gitignore
vendored
Normal file
20
.beads/.gitignore
vendored
Normal 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
56
.beads/config.yaml
Normal 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
5
.beads/metadata.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"database": "beads.db",
|
||||||
|
"version": "0.23.0",
|
||||||
|
"jsonl_export": "beads.jsonl"
|
||||||
|
}
|
||||||
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
# Use bd merge for beads JSONL files
|
||||||
|
.beads/beads.jsonl merge=beads
|
||||||
58
flake.lock
58
flake.lock
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -201,12 +201,21 @@
|
||||||
<div class="diff-viewer-modal">
|
<div class="diff-viewer-modal">
|
||||||
<div class="diff-viewer-header">
|
<div class="diff-viewer-header">
|
||||||
<h2 class="modal-title" id="diff-viewer-title">Git Diff Viewer</h2>
|
<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>
|
<button id="close-diff-viewer" class="diff-viewer-close" title="Close">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="diff-viewer-content">
|
<div class="diff-viewer-content">
|
||||||
<!-- File List Sidebar -->
|
<!-- File List Sidebar -->
|
||||||
<div class="diff-file-list" id="diff-file-list">
|
<div class="diff-file-list" id="diff-file-list">
|
||||||
|
<button id="toggle-diff-sidebar" class="diff-sidebar-toggle">
|
||||||
|
<span class="toggle-expand">>></span>
|
||||||
|
<span class="toggle-collapse"><<</span>
|
||||||
|
</button>
|
||||||
<div class="diff-loading">Loading diff...</div>
|
<div class="diff-loading">Loading diff...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
124
renderer.ts
124
renderer.ts
|
|
@ -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, FileDiff} 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";
|
||||||
|
|
||||||
|
|
@ -1522,6 +1522,8 @@ document.getElementById("session-list")?.addEventListener("click", (e) => {
|
||||||
// Diff Viewer
|
// Diff Viewer
|
||||||
let currentDiffFiles: FileDiff[] = [];
|
let currentDiffFiles: FileDiff[] = [];
|
||||||
let selectedFileIndex: number = -1;
|
let selectedFileIndex: number = -1;
|
||||||
|
let currentDiffSessionId: string = '';
|
||||||
|
let diffSidebarCollapsed: boolean = false;
|
||||||
let pierreLib: any = null;
|
let pierreLib: any = null;
|
||||||
let shikiLib: any = null;
|
let shikiLib: any = null;
|
||||||
let highlighter: any = null;
|
let highlighter: any = null;
|
||||||
|
|
@ -1530,14 +1532,31 @@ async function openDiffViewer(sessionId: string) {
|
||||||
const modal = document.getElementById("diff-viewer-modal");
|
const modal = document.getElementById("diff-viewer-modal");
|
||||||
const fileList = document.getElementById("diff-file-list");
|
const fileList = document.getElementById("diff-file-list");
|
||||||
const diffDisplay = document.getElementById("diff-display");
|
const diffDisplay = document.getElementById("diff-display");
|
||||||
|
const mergeBtn = document.getElementById("merge-to-parent-btn") as HTMLButtonElement;
|
||||||
|
|
||||||
if (!modal || !fileList || !diffDisplay) return;
|
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
|
// Show modal with loading state
|
||||||
modal.classList.remove("hidden");
|
modal.classList.remove("hidden");
|
||||||
fileList.innerHTML = '<div class="diff-loading">Loading diff...</div>';
|
fileList.innerHTML = '<div class="diff-loading">Loading diff...</div>';
|
||||||
diffDisplay.innerHTML = '<div class="diff-empty">Loading...</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)
|
// Load libraries dynamically (only in browser)
|
||||||
if (!pierreLib) {
|
if (!pierreLib) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1560,8 +1579,9 @@ async function openDiffViewer(sessionId: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request diff from server
|
// Request diff and merge status from server
|
||||||
ipcAdapter.send("get-session-diff", sessionId);
|
ipcAdapter.send("get-session-diff", sessionId);
|
||||||
|
ipcAdapter.send("check-merge-status", sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDiffFileList() {
|
function renderDiffFileList() {
|
||||||
|
|
@ -1569,11 +1589,11 @@ function renderDiffFileList() {
|
||||||
if (!fileList) return;
|
if (!fileList) return;
|
||||||
|
|
||||||
if (currentDiffFiles.length === 0) {
|
if (currentDiffFiles.length === 0) {
|
||||||
fileList.innerHTML = '<div class="diff-loading">No changes found</div>';
|
fileList.innerHTML = '<button id="toggle-diff-sidebar" class="diff-sidebar-toggle"><span class="toggle-expand">>></span><span class="toggle-collapse"><<</span></button><div class="diff-loading">No changes found</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileList.innerHTML = '';
|
fileList.innerHTML = '<button id="toggle-diff-sidebar" class="diff-sidebar-toggle"><span class="toggle-expand">>></span><span class="toggle-collapse"><<</span></button>';
|
||||||
|
|
||||||
currentDiffFiles.forEach((file, index) => {
|
currentDiffFiles.forEach((file, index) => {
|
||||||
const item = document.createElement("div");
|
const item = document.createElement("div");
|
||||||
|
|
@ -1615,12 +1635,37 @@ function renderDiffFileList() {
|
||||||
|
|
||||||
item.addEventListener("click", () => {
|
item.addEventListener("click", () => {
|
||||||
selectedFileIndex = index;
|
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();
|
renderDiffFileList();
|
||||||
renderSelectedDiff();
|
renderSelectedDiff();
|
||||||
});
|
});
|
||||||
|
|
||||||
fileList.appendChild(item);
|
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() {
|
async function renderSelectedDiff() {
|
||||||
|
|
@ -1681,12 +1726,33 @@ async function renderSelectedDiff() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Close diff viewer
|
// Close diff viewer
|
||||||
document.getElementById("close-diff-viewer")?.addEventListener("click", () => {
|
document.getElementById("close-diff-viewer")?.addEventListener("click", () => {
|
||||||
const modal = document.getElementById("diff-viewer-modal");
|
const modal = document.getElementById("diff-viewer-modal");
|
||||||
modal?.classList.add("hidden");
|
modal?.classList.add("hidden");
|
||||||
currentDiffFiles = [];
|
currentDiffFiles = [];
|
||||||
selectedFileIndex = -1;
|
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
|
// Handle diff data from server
|
||||||
|
|
@ -1709,3 +1775,53 @@ ipcAdapter.on("session-diff-error", (_event, sessionId: string, error: string) =
|
||||||
diffDisplay.innerHTML = `<div class="diff-empty">Failed to load diff</div>`;
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
103
server.ts
103
server.ts
|
|
@ -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, FileDiff } 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";
|
||||||
|
|
||||||
|
|
@ -630,6 +630,107 @@ io.on("connection", (socket) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
||||||
54
styles.css
54
styles.css
|
|
@ -349,6 +349,18 @@
|
||||||
@apply flex items-center justify-between p-4 border-b border-gray-700;
|
@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 {
|
.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;
|
@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;
|
||||||
}
|
}
|
||||||
|
|
@ -358,7 +370,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.diff-file-list {
|
.diff-file-list {
|
||||||
@apply w-80 border-r border-gray-700 overflow-y-auto bg-gray-800;
|
@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 {
|
.diff-loading {
|
||||||
|
|
@ -409,8 +425,42 @@
|
||||||
@apply text-red-400;
|
@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 {
|
.diff-display {
|
||||||
@apply flex-1 overflow-auto bg-gray-900 p-4;
|
@apply flex-1 overflow-auto bg-gray-900 p-4 relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diff-empty {
|
.diff-empty {
|
||||||
|
|
|
||||||
12
types.ts
12
types.ts
|
|
@ -28,3 +28,15 @@ export interface FileDiff {
|
||||||
deletions: number;
|
deletions: number;
|
||||||
binary?: boolean;
|
binary?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MergeStatus {
|
||||||
|
hasCommits: boolean;
|
||||||
|
commitsAhead: number;
|
||||||
|
hasUncommittedChanges: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MergeResult {
|
||||||
|
success: boolean;
|
||||||
|
conflicts?: string[];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue