/** * VS Code Git API implementation * Uses bridge messages to communicate with the extension host */ import { sendBridgeMessage } from './bridge'; import type { GitAPI, GitStatus, GitDiffResponse, GetGitDiffOptions, GitFileDiffResponse, GetGitFileDiffOptions, GitBranch, GitDeleteBranchPayload, GitDeleteRemoteBranchPayload, GitRemoveRemotePayload, GeneratedCommitMessage, GeneratedPullRequestDescription, GitWorktreeInfo, GitWorktreeBootstrapStatus, CreateGitWorktreePayload, GitWorktreeValidationResult, GitWorktreeCreateResult, RemoveGitWorktreePayload, GitCommitResult, CreateGitCommitOptions, GitPushResult, GitPullResult, GitLogResponse, GitLogOptions, GitCommitFilesResponse, CommitFileDiffResponse, GitIdentitySummary, GitIdentityProfile, GitRemote, GitRebaseResult, GitMergeResult, CheckoutCommitResponse, CherryPickResponse, RevertCommitResponse, ResetToCommitResponse, } from '@openchamber/ui/lib/api/types'; const requestWorktreeBootstrapStatus = (directory: string): Promise => { return sendBridgeMessage('api:git/worktrees/bootstrap-status', { directory }); }; type GitIdentityStoreState = { profiles: GitIdentityProfile[]; }; type GitIdentityStoreApi = { getState: () => GitIdentityStoreState; setState: ( nextState: GitIdentityStoreState | ((state: GitIdentityStoreState) => GitIdentityStoreState), replace?: boolean ) => void; }; const getGitIdentityStore = (): GitIdentityStoreApi | undefined => ( window as Window & { __zustand_git_identities_store__?: GitIdentityStoreApi; } ).__zustand_git_identities_store__; export const createVSCodeGitAPI = (): GitAPI => ({ checkIsGitRepository: async (directory: string): Promise => { return sendBridgeMessage('api:git/check', { directory }); }, getGitStatus: async (directory: string, options?: { mode?: 'light'; fresh?: boolean }): Promise => { return sendBridgeMessage('api:git/status', { directory, mode: options?.mode }); }, getGitDiff: async (directory: string, options: GetGitDiffOptions): Promise => { return sendBridgeMessage('api:git/diff', { directory, path: options.path, staged: options.staged, contextLines: options.contextLines, }); }, getGitFileDiff: async (directory: string, options: GetGitFileDiffOptions): Promise => { return sendBridgeMessage('api:git/file-diff', { directory, path: options.path, staged: options.staged, }); }, revertGitFile: async (directory: string, filePath: string, options?: { scope?: 'all' | 'working' }): Promise => { await sendBridgeMessage('api:git/revert', { directory, path: filePath, scope: options?.scope }); }, stageGitFile: async (directory: string, filePath: string): Promise => { await sendBridgeMessage('api:git/stage', { directory, path: filePath }); }, stageGitFiles: async (directory: string, filePaths: string[]): Promise => { await sendBridgeMessage('api:git/stage', { directory, paths: filePaths }); }, unstageGitFile: async (directory: string, filePath: string): Promise => { await sendBridgeMessage('api:git/unstage', { directory, path: filePath }); }, unstageGitFiles: async (directory: string, filePaths: string[]): Promise => { await sendBridgeMessage('api:git/unstage', { directory, paths: filePaths }); }, stageGitHunk: async (directory: string, filePath: string, patch: string): Promise => { await sendBridgeMessage('api:git/apply-hunk', { directory, path: filePath, patch, action: 'stage' }); }, unstageGitHunk: async (directory: string, filePath: string, patch: string): Promise => { await sendBridgeMessage('api:git/apply-hunk', { directory, path: filePath, patch, action: 'unstage' }); }, revertGitHunk: async (directory: string, filePath: string, patch: string): Promise => { await sendBridgeMessage('api:git/apply-hunk', { directory, path: filePath, patch, action: 'discard' }); }, isLinkedWorktree: async (directory: string): Promise => { return sendBridgeMessage('api:git/worktree-type', { directory }); }, getGitBranches: async (directory: string): Promise => { return sendBridgeMessage('api:git/branches', { directory, method: 'GET' }); }, getGitUnpushedBranchCounts: async (directory: string, branches: string[]) => { return sendBridgeMessage('api:git/branch-push-status', { directory, branches }); }, deleteGitBranch: async (directory: string, payload: GitDeleteBranchPayload): Promise<{ success: boolean }> => { return sendBridgeMessage<{ success: boolean }>('api:git/branches', { directory, method: 'DELETE', name: payload.branch, force: payload.force, }); }, deleteRemoteBranch: async (directory: string, payload: GitDeleteRemoteBranchPayload): Promise<{ success: boolean }> => { return sendBridgeMessage<{ success: boolean }>('api:git/remote-branches', { directory, branch: payload.branch, remote: payload.remote, }); }, removeRemote: async (directory: string, payload: GitRemoveRemotePayload): Promise<{ success: boolean }> => { return sendBridgeMessage<{ success: boolean }>('api:git/remotes', { directory, method: 'DELETE', remote: payload.remote, }); }, generateCommitMessage: async ( directory: string, files: string[], options?: { zenModel?: string; providerId?: string; modelId?: string } ): Promise<{ message: GeneratedCommitMessage }> => { // This requires AI integration - stubbed for now void directory; // Unused for now void files; // Unused for now void options; // Unused for now return { message: { subject: '', highlights: [], }, }; }, generatePullRequestDescription: async ( directory: string, payload: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string } ): Promise => { return sendBridgeMessage('api:git/pr-description', { directory, base: payload.base, head: payload.head, context: payload.context, zenModel: payload.zenModel, providerId: payload.providerId, modelId: payload.modelId, }); }, listGitWorktrees: async (directory: string): Promise => { return sendBridgeMessage('api:git/worktrees', { directory, method: 'GET' }); }, validateGitWorktree: async (directory: string, payload: CreateGitWorktreePayload): Promise => { return sendBridgeMessage('api:git/worktrees/validate', { directory, ...(payload || {}), }); }, getGitWorktreeBootstrapStatus: async (directory: string): Promise => { return requestWorktreeBootstrapStatus(directory); }, previewGitWorktree: async (directory: string, payload: CreateGitWorktreePayload): Promise => { return sendBridgeMessage('api:git/worktrees/preview', { directory, method: 'POST', ...(payload || {}), }); }, createGitWorktree: async (directory: string, payload: CreateGitWorktreePayload): Promise => { return sendBridgeMessage('api:git/worktrees', { directory, method: 'POST', ...(payload || {}), }); }, deleteGitWorktree: async (directory: string, payload: RemoveGitWorktreePayload): Promise<{ success: boolean }> => { return sendBridgeMessage<{ success: boolean }>('api:git/worktrees', { directory, method: 'DELETE', body: { directory: payload.directory, deleteLocalBranch: payload.deleteLocalBranch === true, }, }); }, createGitCommit: async (directory: string, message: string, options?: CreateGitCommitOptions): Promise => { return sendBridgeMessage('api:git/commit', { directory, message, addAll: options?.addAll, files: options?.files, stageFiles: options?.stageFiles, }); }, gitPush: async (directory: string, options?: { remote?: string; branch?: string; options?: string[] | Record }): Promise => { return sendBridgeMessage('api:git/push', { directory, remote: options?.remote, branch: options?.branch, options: options?.options, }); }, gitPull: async (directory: string, options?: { remote?: string; branch?: string; rebase?: boolean }): Promise => { return sendBridgeMessage('api:git/pull', { directory, remote: options?.remote, branch: options?.branch, rebase: options?.rebase, }); }, gitFetch: async (directory: string, options?: { remote?: string; branch?: string }): Promise<{ success: boolean }> => { return sendBridgeMessage<{ success: boolean }>('api:git/fetch', { directory, remote: options?.remote, branch: options?.branch, }); }, listGitStashes: async (directory: string) => sendBridgeMessage('api:git/stashes', { directory }), countGitStashFiles: async (directory: string, refs: string[]) => sendBridgeMessage('api:git/stashes/file-counts', { directory, refs }), stashGitChanges: async (directory: string, options?: { message?: string }) => sendBridgeMessage('api:git/stash', { directory, message: options?.message }), applyGitStash: async (directory: string, options: { ref: string }) => sendBridgeMessage('api:git/stash/apply', { directory, ref: options.ref }), popGitStash: async (directory: string, options: { ref: string }) => sendBridgeMessage('api:git/stash/pop', { directory, ref: options.ref }), dropGitStash: async (directory: string, options: { ref: string }) => sendBridgeMessage('api:git/stash/drop', { directory, ref: options.ref }), checkoutBranch: async (directory: string, branch: string): Promise<{ success: boolean; branch: string }> => { return sendBridgeMessage<{ success: boolean; branch: string }>('api:git/checkout', { directory, branch, }); }, createBranch: async (directory: string, name: string, startPoint?: string): Promise<{ success: boolean; branch: string }> => { return sendBridgeMessage<{ success: boolean; branch: string }>('api:git/branches', { directory, method: 'POST', name, startPoint, }); }, renameBranch: async (directory: string, oldName: string, newName: string): Promise<{ success: boolean; branch: string }> => { return sendBridgeMessage<{ success: boolean; branch: string }>('api:git/branches/rename', { directory, method: 'PUT', oldName, newName, }); }, getGitLog: async (directory: string, options?: GitLogOptions): Promise => { return sendBridgeMessage('api:git/log', { directory, maxCount: options?.maxCount, from: options?.from, to: options?.to, file: options?.file, all: options?.all, }); }, getCommitFiles: async (directory: string, hash: string): Promise => { return sendBridgeMessage('api:git/commit-files', { directory, hash, }); }, getCommitFileDiff: async (directory: string, hash: string, filePath: string, isBinary: boolean): Promise => { return sendBridgeMessage('api:git/commit-file-diff', { directory, hash, path: filePath, binary: isBinary, }); }, getCurrentGitIdentity: async (directory: string): Promise => { return sendBridgeMessage('api:git/identity', { directory, method: 'GET', }); }, setGitIdentity: async (directory: string, profileId: string): Promise<{ success: boolean; profile: GitIdentityProfile }> => { const store = (window as Window & { __zustand_git_identities_store__?: { getState: () => { getProfileById: (id: string) => GitIdentityProfile | undefined; }; }; }).__zustand_git_identities_store__; const profile = store?.getState().getProfileById(profileId); if (!profile) { return { success: false, profile: { id: profileId, name: '', userName: '', userEmail: '' }, }; } const result = await sendBridgeMessage<{ success: boolean }>('api:git/identity', { directory, method: 'POST', userName: profile.userName, userEmail: profile.userEmail, sshKey: profile.sshKey ?? null, signCommits: profile.signCommits === true, signingKey: profile.signingKey ?? null, }); return { success: result.success === true, profile, }; }, // Git identity profile management is backed by the webview store in VS Code. getGitIdentities: async (): Promise => { return getGitIdentityStore()?.getState().profiles ?? []; }, createGitIdentity: async (profile: GitIdentityProfile): Promise => { const store = getGitIdentityStore(); if (store) { store.setState((state) => ({ profiles: [...state.profiles.filter((existing) => existing.id !== profile.id), profile], })); } return profile; }, updateGitIdentity: async (id: string, profile: GitIdentityProfile): Promise => { const store = getGitIdentityStore(); if (store) { store.setState((state) => ({ profiles: state.profiles.map((existing) => (existing.id === id ? { ...existing, ...profile, id } : existing)), })); } return profile; }, deleteGitIdentity: async (id: string): Promise => { const store = getGitIdentityStore(); if (store) { store.setState((state) => ({ profiles: state.profiles.filter((existing) => existing.id !== id), })); } }, getRemotes: async (directory: string): Promise => { return sendBridgeMessage('api:git/remotes', { directory }); }, rebase: async (directory: string, options: { onto: string }): Promise => { return sendBridgeMessage('api:git/rebase', { directory, onto: options.onto, }); }, abortRebase: async (directory: string): Promise<{ success: boolean }> => { return sendBridgeMessage<{ success: boolean }>('api:git/rebase/abort', { directory }); }, merge: async (directory: string, options: { branch: string }): Promise => { return sendBridgeMessage('api:git/merge', { directory, branch: options.branch, }); }, abortMerge: async (directory: string): Promise<{ success: boolean }> => { return sendBridgeMessage<{ success: boolean }>('api:git/merge/abort', { directory }); }, continueRebase: async (directory: string): Promise<{ success: boolean; conflict: boolean; conflictFiles?: string[] }> => { return sendBridgeMessage<{ success: boolean; conflict: boolean; conflictFiles?: string[] }>('api:git/rebase/continue', { directory }); }, continueMerge: async (directory: string): Promise<{ success: boolean; conflict: boolean; conflictFiles?: string[] }> => { return sendBridgeMessage<{ success: boolean; conflict: boolean; conflictFiles?: string[] }>('api:git/merge/continue', { directory }); }, checkoutCommit: async (directory: string, hash: string): Promise => { return sendBridgeMessage('api:git/checkout-commit', { directory, hash }); }, cherryPick: async (directory: string, hash: string): Promise => { return sendBridgeMessage('api:git/cherry-pick', { directory, hash }); }, revertCommit: async (directory: string, hash: string): Promise => { return sendBridgeMessage('api:git/revert-commit', { directory, hash }); }, resetToCommit: async (directory: string, hash: string, mode: 'soft' | 'mixed' | 'hard', force?: boolean): Promise => { return sendBridgeMessage('api:git/reset-to-commit', { directory, hash, mode, force }); }, stash: async ( directory: string, options?: { message?: string; includeUntracked?: boolean } ): Promise<{ success: boolean }> => { return sendBridgeMessage<{ success: boolean }>('api:git/stash', { directory, ...options, }); }, stashPop: async (directory: string): Promise<{ success: boolean }> => { return sendBridgeMessage<{ success: boolean }>('api:git/stash/pop', { directory }); }, getConflictDetails: async (directory: string) => { return sendBridgeMessage<{ statusPorcelain: string; unmergedFiles: string[]; diff: string; headInfo: string; operation: 'merge' | 'rebase'; }>('api:git/conflict-details', { directory }); }, validateWorktreeDirectory: async (directory: string, worktreeRoot: string): Promise<{ valid: boolean; insideWorktreeRoot: boolean; resolvedWorktreeRoot: string | null; resolvedCwd: string | null; }> => { return sendBridgeMessage<{ valid: boolean; insideWorktreeRoot: boolean; resolvedWorktreeRoot: string | null; resolvedCwd: string | null; }>('api:git/validate-directory', { directory, worktreeRoot }); }, canonicalizeWorktreeState: async (directory: string): Promise<{ worktreeRoot: string | null; cwd: string | null; branch: string | null; headState: 'branch' | 'detached' | 'unborn'; worktreeStatus: 'ready' | 'missing' | 'invalid' | 'not-a-repo'; legacy: boolean; degraded: boolean; attentionReason?: 'merge' | 'rebase' | 'cherry-pick' | 'revert' | 'bisect' | null; }> => { return sendBridgeMessage<{ worktreeRoot: string | null; cwd: string | null; branch: string | null; headState: 'branch' | 'detached' | 'unborn'; worktreeStatus: 'ready' | 'missing' | 'invalid' | 'not-a-repo'; legacy: boolean; degraded: boolean; attentionReason?: 'merge' | 'rebase' | 'cherry-pick' | 'revert' | 'bisect' | null; }>('api:git/canonicalize-worktree-state', { directory }); }, worktree: { list: async (directory: string): Promise => { return sendBridgeMessage('api:git/worktrees', { directory, method: 'GET' }); }, validate: async (directory: string, payload: CreateGitWorktreePayload): Promise => { return sendBridgeMessage('api:git/worktrees/validate', { directory, ...(payload || {}), }); }, bootstrapStatus: async (directory: string): Promise => { return requestWorktreeBootstrapStatus(directory); }, preview: async (directory: string, payload: CreateGitWorktreePayload): Promise => { return sendBridgeMessage('api:git/worktrees/preview', { directory, method: 'POST', ...(payload || {}), }); }, create: async (directory: string, payload: CreateGitWorktreePayload): Promise => { return sendBridgeMessage('api:git/worktrees', { directory, method: 'POST', ...(payload || {}), }); }, remove: async (directory: string, payload: RemoveGitWorktreePayload): Promise<{ success: boolean }> => { return sendBridgeMessage<{ success: boolean }>('api:git/worktrees', { directory, method: 'DELETE', body: { directory: payload.directory, deleteLocalBranch: payload.deleteLocalBranch === true, }, }); }, }, });