feat: Address review comments, add stage/unstage functionality, conflict resolution improvements, support for Sonnet 4.6

This commit is contained in:
gsxdsm
2026-02-18 18:58:33 -08:00
parent df9a6314da
commit 983eb21faa
66 changed files with 2317 additions and 823 deletions

View File

@@ -36,6 +36,7 @@ export function formatModelName(model: string): string {
// Claude models
if (model.includes('opus-4-6') || model === 'claude-opus') return 'Opus 4.6';
if (model.includes('opus')) return 'Opus 4.5';
if (model.includes('sonnet-4-6') || model === 'claude-sonnet') return 'Sonnet 4.6';
if (model.includes('sonnet')) return 'Sonnet 4.5';
if (model.includes('haiku')) return 'Haiku 4.5';

View File

@@ -0,0 +1,133 @@
/**
* Shared diff parsing utilities.
*
* Extracted from commit-worktree-dialog, discard-worktree-changes-dialog,
* stash-changes-dialog and git-diff-panel to eliminate duplication.
*/
export interface ParsedDiffHunk {
header: string;
lines: {
type: 'context' | 'addition' | 'deletion' | 'header';
content: string;
lineNumber?: { old?: number; new?: number };
}[];
}
export interface ParsedFileDiff {
filePath: string;
hunks: ParsedDiffHunk[];
isNew?: boolean;
isDeleted?: boolean;
isRenamed?: boolean;
/** Pre-computed count of added lines across all hunks */
additions: number;
/** Pre-computed count of deleted lines across all hunks */
deletions: number;
}
/**
* Parse unified diff format into structured data.
*
* Note: The regex `diff --git a\/(.*?) b\/(.*)` uses a non-greedy match for
* the `a/` path and a greedy match for `b/`. This can mis-handle paths that
* literally contain " b/" or are quoted by git. In practice this covers the
* vast majority of real-world paths; exotic cases will fall back to "unknown".
*/
export function parseDiff(diffText: string): ParsedFileDiff[] {
if (!diffText) return [];
const files: ParsedFileDiff[] = [];
const lines = diffText.split('\n');
let currentFile: ParsedFileDiff | null = null;
let currentHunk: ParsedDiffHunk | null = null;
let oldLineNum = 0;
let newLineNum = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('diff --git')) {
if (currentFile) {
if (currentHunk) currentFile.hunks.push(currentHunk);
files.push(currentFile);
}
const match = line.match(/diff --git a\/(.*?) b\/(.*)/);
currentFile = {
filePath: match ? match[2] : 'unknown',
hunks: [],
additions: 0,
deletions: 0,
};
currentHunk = null;
continue;
}
if (line.startsWith('new file mode')) {
if (currentFile) currentFile.isNew = true;
continue;
}
if (line.startsWith('deleted file mode')) {
if (currentFile) currentFile.isDeleted = true;
continue;
}
if (line.startsWith('rename from') || line.startsWith('rename to')) {
if (currentFile) currentFile.isRenamed = true;
continue;
}
if (line.startsWith('index ') || line.startsWith('--- ') || line.startsWith('+++ ')) {
continue;
}
if (line.startsWith('@@')) {
if (currentHunk && currentFile) currentFile.hunks.push(currentHunk);
const hunkMatch = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
oldLineNum = hunkMatch ? parseInt(hunkMatch[1], 10) : 1;
newLineNum = hunkMatch ? parseInt(hunkMatch[2], 10) : 1;
currentHunk = {
header: line,
lines: [{ type: 'header', content: line }],
};
continue;
}
if (currentHunk) {
// Skip trailing empty line produced by split('\n') to avoid phantom context line
if (line === '' && i === lines.length - 1) {
continue;
}
if (line.startsWith('+')) {
currentHunk.lines.push({
type: 'addition',
content: line.substring(1),
lineNumber: { new: newLineNum },
});
newLineNum++;
if (currentFile) currentFile.additions++;
} else if (line.startsWith('-')) {
currentHunk.lines.push({
type: 'deletion',
content: line.substring(1),
lineNumber: { old: oldLineNum },
});
oldLineNum++;
if (currentFile) currentFile.deletions++;
} else if (line.startsWith(' ') || line === '') {
currentHunk.lines.push({
type: 'context',
content: line.substring(1) || '',
lineNumber: { old: oldLineNum, new: newLineNum },
});
oldLineNum++;
newLineNum++;
}
}
}
if (currentFile) {
if (currentHunk) currentFile.hunks.push(currentHunk);
files.push(currentFile);
}
return files;
}

View File

@@ -2259,6 +2259,17 @@ function createMockWorktreeAPI(): WorktreeAPI {
};
},
stageFiles: async (worktreePath: string, files: string[], operation: 'stage' | 'unstage') => {
console.log('[Mock] Stage files:', { worktreePath, files, operation });
return {
success: true,
result: {
operation,
filesCount: files.length,
},
};
},
pull: async (worktreePath: string, remote?: string, stashIfNeeded?: boolean) => {
const targetRemote = remote || 'origin';
console.log('[Mock] Pulling latest changes for:', {
@@ -2760,6 +2771,28 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
};
},
abortOperation: async (worktreePath: string) => {
console.log('[Mock] Abort operation:', { worktreePath });
return {
success: true,
result: {
operation: 'merge',
message: 'Merge aborted successfully',
},
};
},
continueOperation: async (worktreePath: string) => {
console.log('[Mock] Continue operation:', { worktreePath });
return {
success: true,
result: {
operation: 'merge',
message: 'Merge continued successfully',
},
};
},
};
}
@@ -2787,6 +2820,17 @@ function createMockGitAPI(): GitAPI {
filePath,
};
},
stageFiles: async (projectPath: string, files: string[], operation: 'stage' | 'unstage') => {
console.log('[Mock] Git stage files:', { projectPath, files, operation });
return {
success: true,
result: {
operation,
filesCount: files.length,
},
};
},
};
}

View File

@@ -2135,6 +2135,8 @@ export class HttpApiClient implements ElectronAPI {
featureId,
filePath,
}),
stageFiles: (worktreePath: string, files: string[], operation: 'stage' | 'unstage') =>
this.post('/api/worktree/stage-files', { worktreePath, files, operation }),
pull: (worktreePath: string, remote?: string, stashIfNeeded?: boolean) =>
this.post('/api/worktree/pull', { worktreePath, remote, stashIfNeeded }),
checkoutBranch: (worktreePath: string, branchName: string, baseBranch?: string) =>
@@ -2232,6 +2234,10 @@ export class HttpApiClient implements ElectronAPI {
this.post('/api/worktree/cherry-pick', { worktreePath, commitHashes, options }),
rebase: (worktreePath: string, ontoBranch: string) =>
this.post('/api/worktree/rebase', { worktreePath, ontoBranch }),
abortOperation: (worktreePath: string) =>
this.post('/api/worktree/abort-operation', { worktreePath }),
continueOperation: (worktreePath: string) =>
this.post('/api/worktree/continue-operation', { worktreePath }),
getBranchCommitLog: (worktreePath: string, branchName?: string, limit?: number) =>
this.post('/api/worktree/branch-commit-log', { worktreePath, branchName, limit }),
getTestLogs: (worktreePath?: string, sessionId?: string): Promise<TestLogsResponse> => {
@@ -2263,6 +2269,8 @@ export class HttpApiClient implements ElectronAPI {
getDiffs: (projectPath: string) => this.post('/api/git/diffs', { projectPath }),
getFileDiff: (projectPath: string, filePath: string) =>
this.post('/api/git/file-diff', { projectPath, filePath }),
stageFiles: (projectPath: string, files: string[], operation: 'stage' | 'unstage') =>
this.post('/api/git/stage-files', { projectPath, files, operation }),
};
// Spec Regeneration API