import * as gitHttp from './gitApiHttp'; import { opencodeClient } from './opencode/client'; import { renderMagicPrompt } from './magicPrompts'; import { runtimeFetch } from './runtime-fetch'; import { materializeOpenDraftSession, useSessionUIStore } from '@/sync/session-ui-store'; import { useSelectionStore } from '@/sync/selection-store'; import { useConfigStore } from '@/stores/useConfigStore'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; export type { GitStatus, GitDiffResponse, GetGitDiffOptions, GitBranchDetails, GitBranch, GitCommitResult, GitPushResult, GitPullResult, GitIdentityProfile, GitIdentityAuthType, GitIdentitySummary, GitLogEntry, GitLogResponse, GitWorktreeInfo, CreateGitWorktreePayload, GitWorktreeCreateResult, RemoveGitWorktreePayload, GitWorktreeValidationError, GitWorktreeValidationResult, GitDeleteBranchPayload, GitDeleteRemoteBranchPayload, GitRemoveRemotePayload, DiscoveredGitCredential, GitRemote, GitMergeResult, GitRebaseResult, MergeConflictDetails, CommitFileDiffResponse, } from './api/types'; const getRuntimeGit = () => { return getRegisteredRuntimeAPIs()?.git ?? null; }; const requestChatForceScrollBottom = (sessionId: string) => { if (typeof window === 'undefined') return; window.dispatchEvent(new CustomEvent('openchamber:chat-force-scroll-bottom', { detail: { sessionId }, })); }; const extractJsonObject = (value: string): Record | null => { const text = value.trim(); const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i); const candidate = (fenced?.[1] ?? text).trim(); const starts = [candidate.indexOf('{')].filter((index) => index >= 0); for (const start of starts) { for (let end = candidate.length; end > start; end -= 1) { if (candidate[end - 1] !== '}') continue; try { const parsed = JSON.parse(candidate.slice(start, end)) as unknown; if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { return parsed as Record; } } catch { // Keep scanning; models sometimes wrap JSON with prose or fences. } } } return null; }; const extractAssistantText = (response: unknown): string => { const data = (response as { data?: { parts?: Array } } | null)?.data; const parts = Array.isArray(data?.parts) ? data.parts : []; return parts .map((part) => { const item = part as { type?: unknown; text?: unknown; content?: unknown; value?: unknown }; if (item.type !== 'text') return ''; if (typeof item.text === 'string') return item.text; if (typeof item.content === 'string') return item.content; if (typeof item.value === 'string') return item.value; return ''; }) .filter((text) => text.trim().length > 0) .join('\n') .trim(); }; export async function checkIsGitRepository(directory: string): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.checkIsGitRepository(directory); return gitHttp.checkIsGitRepository(directory); } export async function getGitStatus(directory: string, options?: { mode?: 'light' }): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.getGitStatus(directory, options); return gitHttp.getGitStatus(directory, options); } export async function resolveGitPrimaryRoot(directory: string): Promise { const result = await gitHttp.resolveGitPrimaryRoot(directory); return result.root; } export async function resolveGitTopLevel(directory: string): Promise { const result = await gitHttp.resolveGitTopLevel(directory); return result.root; } export async function getGitCommitSummaries( directory: string, shas: string[] ): Promise> { const result = await gitHttp.getGitCommitSummaries(directory, shas); return result.commits; } export async function getGitDiff(directory: string, options: import('./api/types').GetGitDiffOptions): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.getGitDiff(directory, options); return gitHttp.getGitDiff(directory, options); } export async function getGitFileDiff( directory: string, options: import('./api/types').GetGitFileDiffOptions ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.getGitFileDiff(directory, options); return gitHttp.getGitFileDiff(directory, options); } export async function getGitRangeDiff( directory: string, options: import('./api/types').GetGitRangeDiffOptions ): Promise { const runtime = getRuntimeGit(); if (runtime?.getGitRangeDiff) return runtime.getGitRangeDiff(directory, options); return gitHttp.getGitRangeDiff(directory, options); } export async function revertGitFile( directory: string, filePath: string, options?: { scope?: 'all' | 'working' } ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.revertGitFile(directory, filePath, options); return gitHttp.revertGitFile(directory, filePath, options); } export async function stageGitFile(directory: string, filePath: string): Promise { const runtime = getRuntimeGit(); if (runtime?.stageGitFile) return runtime.stageGitFile(directory, filePath); return gitHttp.stageGitFile(directory, filePath); } export async function stageGitFiles(directory: string, filePaths: string[]): Promise { const runtime = getRuntimeGit(); if (runtime?.stageGitFiles) return runtime.stageGitFiles(directory, filePaths); return gitHttp.stageGitFiles(directory, filePaths); } export async function unstageGitFile(directory: string, filePath: string): Promise { const runtime = getRuntimeGit(); if (runtime?.unstageGitFile) return runtime.unstageGitFile(directory, filePath); return gitHttp.unstageGitFile(directory, filePath); } export async function unstageGitFiles(directory: string, filePaths: string[]): Promise { const runtime = getRuntimeGit(); if (runtime?.unstageGitFiles) return runtime.unstageGitFiles(directory, filePaths); return gitHttp.unstageGitFiles(directory, filePaths); } export async function stageGitHunk(directory: string, filePath: string, patch: string): Promise { const runtime = getRuntimeGit(); if (runtime?.stageGitHunk) return runtime.stageGitHunk(directory, filePath, patch); return gitHttp.stageGitHunk(directory, filePath, patch); } export async function unstageGitHunk(directory: string, filePath: string, patch: string): Promise { const runtime = getRuntimeGit(); if (runtime?.unstageGitHunk) return runtime.unstageGitHunk(directory, filePath, patch); return gitHttp.unstageGitHunk(directory, filePath, patch); } export async function revertGitHunk(directory: string, filePath: string, patch: string): Promise { const runtime = getRuntimeGit(); if (runtime?.revertGitHunk) return runtime.revertGitHunk(directory, filePath, patch); return gitHttp.revertGitHunk(directory, filePath, patch); } export async function isLinkedWorktree(directory: string): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.isLinkedWorktree(directory); return gitHttp.isLinkedWorktree(directory); } export async function getGitBranches(directory: string): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.getGitBranches(directory); return gitHttp.getGitBranches(directory); } export async function deleteGitBranch(directory: string, payload: import('./api/types').GitDeleteBranchPayload): Promise<{ success: boolean }> { const runtime = getRuntimeGit(); if (runtime) return runtime.deleteGitBranch(directory, payload); return gitHttp.deleteGitBranch(directory, payload); } export async function deleteRemoteBranch(directory: string, payload: import('./api/types').GitDeleteRemoteBranchPayload): Promise<{ success: boolean }> { const runtime = getRuntimeGit(); if (runtime) return runtime.deleteRemoteBranch(directory, payload); return gitHttp.deleteRemoteBranch(directory, payload); } const COMMIT_DIFF_FILE_LIMIT = 30; const COMMIT_DIFF_TOTAL_CHAR_LIMIT = 120_000; const collectSelectedFileDiffs = async (directory: string, files: string[]): Promise => { const limited = files.slice(0, COMMIT_DIFF_FILE_LIMIT); const chunks = await Promise.all(limited.map(async (path) => { try { const [staged, unstaged] = await Promise.all([ gitHttp.getGitDiff(directory, { path, staged: true }).catch(() => null), gitHttp.getGitDiff(directory, { path, staged: false }).catch(() => null), ]); const text = [staged?.diff, unstaged?.diff] .filter((diff): diff is string => typeof diff === 'string' && diff.trim().length > 0) .join('\n'); return text ? text : `--- ${path} (no textual diff available)`; } catch { return `--- ${path} (diff unavailable)`; } })); let total = ''; for (const chunk of chunks) { if (total.length + chunk.length > COMMIT_DIFF_TOTAL_CHAR_LIMIT) { total += '\n[remaining diffs truncated]'; break; } total += (total ? '\n\n' : '') + chunk; } if (files.length > limited.length) { total += `\n[${files.length - limited.length} more selected files omitted]`; } return total; }; const parseCommitStructured = (structured: Record | null): { subject: string; highlights: string[] } => { const subject = typeof structured?.subject === 'string' ? structured.subject.trim() : ''; const highlights = Array.isArray(structured?.highlights) ? structured.highlights.filter((item) => typeof item === 'string').map((item) => item.trim()).filter(Boolean).slice(0, 3) : []; if (!subject) { throw new Error('Structured output missing subject'); } return { subject, highlights }; }; // Legacy transport: run the structured generation inside the active chat // session. Kept as the fallback for setups with no direct provider login // (vanilla installs on OpenCode's free models), where the small-model // endpoint has nothing to call but the session itself still works. async function generateCommitMessageViaSession( directory: string, visiblePrompt: string, hiddenPrompt: string, ): Promise<{ message: import('./api/types').GeneratedCommitMessage }> { const generationSession = await resolveGenerationSessionContext(); const structured = await runStructuredGenerationInActiveSession({ directory, visiblePrompt, hiddenPrompt, generationSession, kind: 'commit', }); return { message: parseCommitStructured(structured) }; } export async function generateCommitMessage( directory: string, files: string[], options?: { zenModel?: string; providerId?: string; modelId?: string } ): Promise<{ message: import('./api/types').GeneratedCommitMessage }> { const startedAt = Date.now(); void options; console.info('[git-generation][browser] request', { transport: 'small-model', kind: 'commit', directory, selectedFiles: files.length, }); const visiblePrompt = await renderMagicPrompt('git.commit.generate.visible'); const hiddenPrompt = await renderMagicPrompt('git.commit.generate.instructions', { selected_files: files.map((file) => `- ${file}`).join('\n'), }); try { const diffs = await collectSelectedFileDiffs(directory, files); const { currentProviderId, currentModelId } = useConfigStore.getState(); const response = await runtimeFetch('/api/small-model/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ system: visiblePrompt, prompt: `${hiddenPrompt}\n\nDiffs of the selected files:\n${diffs}`, directory, ...(currentProviderId ? { preferredProviderID: currentProviderId } : {}), ...(currentModelId ? { preferredModelID: currentModelId } : {}), }), }); if (response.status === 404) { // No authenticated provider has a small model — fall back to the // session transport so free-model-only setups keep a working button. console.info('[git-generation][browser] small model unavailable, falling back to session transport'); const result = await generateCommitMessageViaSession(directory, visiblePrompt, hiddenPrompt); console.info('[git-generation][browser] success', { transport: 'session-fallback', kind: 'commit', elapsedMs: Date.now() - startedAt, subjectLength: result.message.subject.length, highlightsCount: result.message.highlights.length, }); return result; } const payload = await response.json().catch(() => null) as { text?: unknown; error?: unknown } | null; if (!response.ok || typeof payload?.text !== 'string') { const message = typeof payload?.error === 'string' ? payload.error : `HTTP ${response.status}`; throw new Error(message); } const result = { message: parseCommitStructured(extractJsonObject(payload.text)) }; console.info('[git-generation][browser] success', { transport: 'small-model', kind: 'commit', elapsedMs: Date.now() - startedAt, subjectLength: result.message.subject.length, highlightsCount: result.message.highlights.length, }); return result; } catch (error) { console.error('[git-generation][browser] failed', { transport: 'small-model', kind: 'commit', elapsedMs: Date.now() - startedAt, message: error instanceof Error ? error.message : String(error), error, }); throw error; } } export async function generatePullRequestDescription( directory: string, payload: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string } ): Promise { const startedAt = Date.now(); const commitLog = await getGitLog(directory, { from: payload.base, to: payload.head, maxCount: 50, }); const COMMIT_BODY_CHAR_LIMIT = 2_000; const commits = (Array.isArray(commitLog?.all) ? commitLog.all : []) .filter((entry) => typeof entry?.hash === 'string' && entry.hash.length > 0) .map((entry) => ({ hash: entry.hash, subject: typeof entry.message === 'string' ? entry.message.trim() : '', body: typeof entry.body === 'string' ? entry.body.trim().slice(0, COMMIT_BODY_CHAR_LIMIT) : '', })); if (commits.length === 0) { throw new Error(`No commits found in range ${payload.base}...${payload.head}`); } const filesSet = new Set(); await Promise.all(commits.map(async (commit) => { try { const response = await getCommitFiles(directory, commit.hash); const files = Array.isArray(response?.files) ? response.files : []; for (const file of files) { if (typeof file?.path === 'string' && file.path.trim().length > 0) { filesSet.add(file.path.trim()); } } } catch (error) { console.warn('[git-generation][browser] failed to collect commit files', { hash: commit.hash, error: error instanceof Error ? error.message : String(error), }); } })); const changedFiles = Array.from(filesSet).sort().slice(0, 300); console.info('[git-generation][browser] request', { transport: 'small-model', kind: 'pr', directory, base: payload.base, head: payload.head, commits: commits.length, changedFiles: changedFiles.length, }); const visiblePrompt = await renderMagicPrompt('git.pr.generate.visible'); const hiddenPrompt = await renderMagicPrompt('git.pr.generate.instructions', { base_branch: payload.base, head_branch: payload.head, commits: commits.map((commit) => { const line = `- ${commit.hash.slice(0, 7)} ${commit.subject || '(no subject)'}`; if (!commit.body) return line; const indentedBody = commit.body.split('\n').map((bodyLine) => ` ${bodyLine}`).join('\n'); return `${line}\n${indentedBody}`; }).join('\n'), changed_files: changedFiles.length > 0 ? changedFiles.map((file) => `- ${file}`).join('\n') : '- none detected', additional_context_block: payload.context?.trim() ? `\nAdditional context:\n${payload.context.trim()}` : '', }); const parsePrStructured = (structured: Record | null) => ({ title: typeof structured?.title === 'string' ? structured.title.trim() : '', body: typeof structured?.body === 'string' ? structured.body.trim() : '', }); try { const { currentProviderId, currentModelId } = useConfigStore.getState(); const response = await runtimeFetch('/api/small-model/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ system: visiblePrompt, prompt: hiddenPrompt, directory, ...(currentProviderId ? { preferredProviderID: currentProviderId } : {}), ...(currentModelId ? { preferredModelID: currentModelId } : {}), }), }); if (response.status === 404) { // No authenticated provider has a small model — fall back to the // session transport so free-model-only setups keep working. console.info('[git-generation][browser] small model unavailable, falling back to session transport'); const generationSession = await resolveGenerationSessionContext(); const structured = await runStructuredGenerationInActiveSession({ directory, visiblePrompt, hiddenPrompt, generationSession, kind: 'pr', }); const result = parsePrStructured(structured); console.info('[git-generation][browser] success', { transport: 'session-fallback', kind: 'pr', elapsedMs: Date.now() - startedAt, titleLength: result.title.length, bodyLength: result.body.length, }); return result; } const payload = await response.json().catch(() => null) as { text?: unknown; error?: unknown } | null; if (!response.ok || typeof payload?.text !== 'string') { const message = typeof payload?.error === 'string' ? payload.error : `HTTP ${response.status}`; throw new Error(message); } const result = parsePrStructured(extractJsonObject(payload.text)); console.info('[git-generation][browser] success', { transport: 'small-model', kind: 'pr', elapsedMs: Date.now() - startedAt, titleLength: result.title.length, bodyLength: result.body.length, }); return result; } catch (error) { console.error('[git-generation][browser] failed', { transport: 'small-model', kind: 'pr', elapsedMs: Date.now() - startedAt, message: error instanceof Error ? error.message : String(error), error, }); throw error; } } type SessionGenerationContext = { sessionId: string; providerID: string; modelID: string; agent?: string; variant?: string; }; const GENERATION_CONFIG_ERROR = 'No default provider or model configured. Please select a provider and model in settings first.'; async function resolveGenerationSessionContext(): Promise { const activeSession = resolveSessionGenerationContext(); if (activeSession) { return activeSession; } const draft = useSessionUIStore.getState().newSessionDraft; if (!draft?.open) { throw new Error('Select existing session for generation'); } const config = useConfigStore.getState(); if (!config.currentProviderId || !config.currentModelId) { throw new Error(GENERATION_CONFIG_ERROR); } const createdDraftSession = await materializeOpenDraftSession({ providerID: config.currentProviderId, modelID: config.currentModelId, agent: config.currentAgentName || undefined, variant: config.currentVariant || undefined, }); if (!createdDraftSession) { const retry = resolveSessionGenerationContext(); if (retry) { return retry; } throw new Error('Failed to create session for generation'); } return { sessionId: createdDraftSession.sessionId, providerID: config.currentProviderId, modelID: config.currentModelId, agent: createdDraftSession.agent, variant: config.currentVariant || undefined, }; } const resolveSessionGenerationContext = (): SessionGenerationContext | null => { const sessionId = useSessionUIStore.getState().currentSessionId; if (!sessionId) { return null; } const selection = useSelectionStore.getState(); const config = useConfigStore.getState(); const lastChoice = useSessionUIStore.getState().getLastUserChoice(sessionId); const agent = selection.getSessionAgentSelection(sessionId) || lastChoice?.agent || config.currentAgentName || undefined; const sessionModel = selection.getSessionModelSelection(sessionId); const agentModel = agent ? selection.getAgentModelForSession(sessionId, agent) : null; const lastChoiceModel = lastChoice?.providerID && lastChoice.modelID ? { providerId: lastChoice.providerID, modelId: lastChoice.modelID } : null; const selectedModel = agentModel || sessionModel || lastChoiceModel || (config.currentProviderId && config.currentModelId ? { providerId: config.currentProviderId, modelId: config.currentModelId } : null); if (!selectedModel?.providerId || !selectedModel?.modelId) { return null; } const selectionVariant = agent ? selection.getAgentModelVariantForSession(sessionId, agent, selectedModel.providerId, selectedModel.modelId) : undefined; const lastChoiceVariant = lastChoiceModel && lastChoiceModel.providerId === selectedModel.providerId && lastChoiceModel.modelId === selectedModel.modelId ? lastChoice?.variant : undefined; const configVariant = config.currentProviderId === selectedModel.providerId && config.currentModelId === selectedModel.modelId ? config.currentVariant : undefined; const variant = selectionVariant || lastChoiceVariant || configVariant || undefined; return { sessionId, providerID: selectedModel.providerId, modelID: selectedModel.modelId, agent, variant, }; }; const runStructuredGenerationInActiveSession = async ({ directory, visiblePrompt, hiddenPrompt, generationSession, kind, }: { directory: string; visiblePrompt: string; hiddenPrompt?: string; generationSession: SessionGenerationContext; kind: 'commit' | 'pr'; }): Promise> => { const requestStartedAt = Date.now(); console.info('[git-generation][browser] runStructuredGenerationInActiveSession start', { kind, directory, sessionId: generationSession.sessionId, providerID: generationSession.providerID, modelID: generationSession.modelID, agent: generationSession.agent, variant: generationSession.variant, }); const trimmedDirectory = typeof directory === 'string' ? directory.trim() : ''; const visiblePromptText = typeof visiblePrompt === 'string' ? visiblePrompt.trim() : ''; const hiddenPromptText = typeof hiddenPrompt === 'string' ? hiddenPrompt.trim() : ''; const promptParts: Array<{ type: 'text'; text: string; synthetic?: boolean }> = []; if (visiblePromptText) { promptParts.push({ type: 'text', text: hiddenPromptText ? `${visiblePromptText}\n\n` : visiblePromptText, synthetic: false, }); } if (hiddenPromptText) { promptParts.push({ type: 'text', text: hiddenPromptText, synthetic: true }); } if (promptParts.length === 0) { throw new Error('Generation prompts are empty'); } requestChatForceScrollBottom(generationSession.sessionId); const response = await opencodeClient.withDirectory(directory, async () => { return opencodeClient.getApiClient().session.prompt({ sessionID: generationSession.sessionId, ...(trimmedDirectory.length > 0 ? { directory: trimmedDirectory } : {}), model: { providerID: generationSession.providerID, modelID: generationSession.modelID, }, ...(generationSession.agent ? { agent: generationSession.agent } : {}), ...(generationSession.variant ? { variant: generationSession.variant } : {}), parts: promptParts, }); }); const responseError = response?.error as { message?: string } | undefined; if (!response?.data) { throw new Error(responseError?.message || `Failed to generate ${kind} output`); } const info = response.data.info as { finish?: string; error?: unknown }; const assistantText = extractAssistantText(response); const parsedOutput = extractJsonObject(assistantText); if (!parsedOutput) { console.error('[git-generation][browser] invalid JSON output', { kind, sessionId: generationSession.sessionId, elapsedMs: Date.now() - requestStartedAt, finish: info?.finish, assistantText, messageInfo: response.data.info, messageParts: response.data.parts, }); throw new Error('No JSON output returned by session'); } return parsedOutput; }; export async function listGitWorktrees(directory: string): Promise { const runtime = getRuntimeGit(); if (runtime?.worktree?.list) { return runtime.worktree.list(directory); } if (runtime) return runtime.listGitWorktrees(directory); return gitHttp.listGitWorktrees(directory); } export async function validateGitWorktree( directory: string, payload: import('./api/types').CreateGitWorktreePayload ): Promise { const runtime = getRuntimeGit(); if (runtime?.worktree?.validate) { return runtime.worktree.validate(directory, payload); } if (runtime?.validateGitWorktree) { return runtime.validateGitWorktree(directory, payload); } return gitHttp.validateGitWorktree(directory, payload); } export async function getGitWorktreeBootstrapStatus( directory: string, ): Promise { const runtime = getRuntimeGit(); if (runtime?.worktree?.bootstrapStatus) { return runtime.worktree.bootstrapStatus(directory); } if (runtime?.getGitWorktreeBootstrapStatus) { return runtime.getGitWorktreeBootstrapStatus(directory); } return gitHttp.getGitWorktreeBootstrapStatus(directory); } export async function previewGitWorktree( directory: string, payload: import('./api/types').CreateGitWorktreePayload ): Promise { const runtime = getRuntimeGit(); if (runtime?.worktree?.preview) { return runtime.worktree.preview(directory, payload); } if (runtime?.previewGitWorktree) { return runtime.previewGitWorktree(directory, payload); } return gitHttp.previewGitWorktree(directory, payload); } export async function createGitWorktree( directory: string, payload: import('./api/types').CreateGitWorktreePayload ): Promise { const runtime = getRuntimeGit(); if (runtime?.worktree?.create) { return runtime.worktree.create(directory, payload); } if (runtime?.createGitWorktree) { return runtime.createGitWorktree(directory, payload); } return gitHttp.createGitWorktree(directory, payload); } export async function deleteGitWorktree( directory: string, payload: import('./api/types').RemoveGitWorktreePayload ): Promise<{ success: boolean }> { const runtime = getRuntimeGit(); if (runtime?.worktree?.remove) { return runtime.worktree.remove(directory, payload); } if (runtime?.deleteGitWorktree) { return runtime.deleteGitWorktree(directory, payload); } return gitHttp.deleteGitWorktree(directory, payload); } export const git = { worktree: { list: listGitWorktrees, validate: validateGitWorktree, create: createGitWorktree, remove: deleteGitWorktree, }, }; export async function createGitCommit( directory: string, message: string, options: import('./api/types').CreateGitCommitOptions = {} ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.createGitCommit(directory, message, options); return gitHttp.createGitCommit(directory, message, options); } export async function gitPush( directory: string, options: { remote?: string; branch?: string; options?: string[] | Record } = {} ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.gitPush(directory, options); return gitHttp.gitPush(directory, options); } export async function gitPull( directory: string, options: import('./api/types').GitPullOptions = {} ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.gitPull(directory, options); return gitHttp.gitPull(directory, options); } export async function gitFetch( directory: string, options: { remote?: string; branch?: string } = {} ): Promise<{ success: boolean }> { const runtime = getRuntimeGit(); if (runtime) return runtime.gitFetch(directory, options); return gitHttp.gitFetch(directory, options); } export async function listGitStashes(directory: string): Promise<{ stashes: import('./api/types').GitStashEntry[] }> { const runtime = getRuntimeGit(); if (runtime) return runtime.listGitStashes(directory); return gitHttp.listGitStashes(directory); } export async function countGitStashFiles(directory: string, refs: string[]): Promise<{ counts: Record }> { const runtime = getRuntimeGit(); if (runtime) return runtime.countGitStashFiles(directory, refs); return gitHttp.countGitStashFiles(directory, refs); } export async function stashGitChanges(directory: string, options: { message?: string } = {}): Promise<{ success: boolean; created: boolean; message: string; output: string }> { const runtime = getRuntimeGit(); if (runtime) return runtime.stashGitChanges(directory, options); return gitHttp.stashGitChanges(directory, options); } export async function applyGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> { const runtime = getRuntimeGit(); if (runtime) return runtime.applyGitStash(directory, options); return gitHttp.applyGitStash(directory, options); } export async function popGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> { const runtime = getRuntimeGit(); if (runtime) return runtime.popGitStash(directory, options); return gitHttp.popGitStash(directory, options); } export async function dropGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> { const runtime = getRuntimeGit(); if (runtime) return runtime.dropGitStash(directory, options); return gitHttp.dropGitStash(directory, options); } export async function checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }> { const runtime = getRuntimeGit(); if (runtime) return runtime.checkoutBranch(directory, branch); return gitHttp.checkoutBranch(directory, branch); } export async function createBranch( directory: string, name: string, startPoint?: string ): Promise<{ success: boolean; branch: string }> { const runtime = getRuntimeGit(); if (runtime) return runtime.createBranch(directory, name, startPoint); return gitHttp.createBranch(directory, name, startPoint); } export async function renameBranch( directory: string, oldName: string, newName: string ): Promise<{ success: boolean; branch: string }> { const runtime = getRuntimeGit(); if (runtime) return runtime.renameBranch(directory, oldName, newName); return gitHttp.renameBranch(directory, oldName, newName); } export async function getGitLog( directory: string, options: import('./api/types').GitLogOptions = {} ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.getGitLog(directory, options); return gitHttp.getGitLog(directory, options); } export async function getCommitFiles( directory: string, hash: string ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.getCommitFiles(directory, hash); return gitHttp.getCommitFiles(directory, hash); } export async function getCommitFileDiff( directory: string, hash: string, filePath: string, isBinary: boolean ): Promise { const runtime = getRuntimeGit(); if (runtime?.getCommitFileDiff) return runtime.getCommitFileDiff(directory, hash, filePath, isBinary); return gitHttp.getCommitFileDiff(directory, hash, filePath, isBinary); } export async function getGitIdentities(): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.getGitIdentities(); return gitHttp.getGitIdentities(); } export async function createGitIdentity(profile: import('./api/types').GitIdentityProfile): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.createGitIdentity(profile); return gitHttp.createGitIdentity(profile); } export async function updateGitIdentity(id: string, updates: import('./api/types').GitIdentityProfile): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.updateGitIdentity(id, updates); return gitHttp.updateGitIdentity(id, updates); } export async function deleteGitIdentity(id: string): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.deleteGitIdentity(id); return gitHttp.deleteGitIdentity(id); } export async function getCurrentGitIdentity(directory: string): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.getCurrentGitIdentity(directory); return gitHttp.getCurrentGitIdentity(directory); } export async function hasLocalIdentity(directory: string): Promise { const runtime = getRuntimeGit(); if (runtime?.hasLocalIdentity) return runtime.hasLocalIdentity(directory); return gitHttp.hasLocalIdentity(directory); } export async function setGitIdentity( directory: string, profileId: string ): Promise<{ success: boolean; profile: import('./api/types').GitIdentityProfile }> { const runtime = getRuntimeGit(); if (runtime) return runtime.setGitIdentity(directory, profileId); return gitHttp.setGitIdentity(directory, profileId); } export async function discoverGitCredentials(): Promise { const runtime = getRuntimeGit(); if (runtime?.discoverGitCredentials) return runtime.discoverGitCredentials(); return gitHttp.discoverGitCredentials(); } export async function getGlobalGitIdentity(): Promise { const runtime = getRuntimeGit(); if (runtime?.getGlobalGitIdentity) return runtime.getGlobalGitIdentity(); return gitHttp.getGlobalGitIdentity(); } export async function getRemoteUrl(directory: string, remote?: string): Promise { const runtime = getRuntimeGit(); if (runtime?.getRemoteUrl) return runtime.getRemoteUrl(directory, remote); return gitHttp.getRemoteUrl(directory, remote); } export async function getRemotes(directory: string): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.getRemotes(directory); return gitHttp.getRemotes(directory); } export async function removeRemote( directory: string, payload: import('./api/types').GitRemoveRemotePayload ): Promise<{ success: boolean }> { const runtime = getRuntimeGit(); if (runtime) return runtime.removeRemote(directory, payload); return gitHttp.removeRemote(directory, payload); } export async function rebase( directory: string, options: { onto: string } ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.rebase(directory, options); return gitHttp.rebase(directory, options); } export async function abortRebase(directory: string): Promise<{ success: boolean }> { const runtime = getRuntimeGit(); if (runtime) return runtime.abortRebase(directory); return gitHttp.abortRebase(directory); } export async function merge( directory: string, options: { branch: string } ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.merge(directory, options); return gitHttp.merge(directory, options); } export async function checkoutCommit( directory: string, hash: string ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.checkoutCommit(directory, hash); return gitHttp.checkoutCommit(directory, hash); } export async function cherryPick( directory: string, hash: string ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.cherryPick(directory, hash); return gitHttp.cherryPick(directory, hash); } export async function revertCommit( directory: string, hash: string ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.revertCommit(directory, hash); return gitHttp.revertCommit(directory, hash); } export async function resetToCommit( directory: string, hash: string, mode: 'soft' | 'mixed' | 'hard', force?: boolean ): Promise { const runtime = getRuntimeGit(); if (runtime) return runtime.resetToCommit(directory, hash, mode, force); return gitHttp.resetToCommit(directory, hash, mode, force); } export async function abortMerge(directory: string): Promise<{ success: boolean }> { const runtime = getRuntimeGit(); if (runtime) return runtime.abortMerge(directory); return gitHttp.abortMerge(directory); } export async function continueRebase(directory: string): Promise<{ success: boolean; conflict: boolean; conflictFiles?: string[] }> { const runtime = getRuntimeGit(); if (runtime) return runtime.continueRebase(directory); return gitHttp.continueRebase(directory); } export async function continueMerge(directory: string): Promise<{ success: boolean; conflict: boolean; conflictFiles?: string[] }> { const runtime = getRuntimeGit(); if (runtime) return runtime.continueMerge(directory); return gitHttp.continueMerge(directory); } export async function stash( directory: string, options?: { message?: string; includeUntracked?: boolean } ): Promise<{ success: boolean }> { const runtime = getRuntimeGit(); if (runtime) return runtime.stash(directory, options); return gitHttp.stash(directory, options); } export async function stashPop(directory: string): Promise<{ success: boolean }> { const runtime = getRuntimeGit(); if (runtime) return runtime.stashPop(directory); return gitHttp.stashPop(directory); } export async function getConflictDetails(directory: string): Promise { const runtime = getRuntimeGit(); if (runtime?.getConflictDetails) return runtime.getConflictDetails(directory); return gitHttp.getConflictDetails(directory); } export async function validateWorktreeDirectory( directory: string, worktreeRoot: string ): Promise<{ valid: boolean; insideWorktreeRoot: boolean; resolvedWorktreeRoot: string | null; resolvedCwd: string | null; }> { const runtime = getRuntimeGit(); if (runtime?.validateWorktreeDirectory) { return runtime.validateWorktreeDirectory(directory, worktreeRoot); } return gitHttp.validateWorktreeDirectory(directory, worktreeRoot); } export async function canonicalizeWorktreeState( directory: string ): Promise<{ worktreeRoot: string | null; cwd: string | null; branch: string | null; headState: 'branch' | 'detached' | 'unborn'; worktreeStatus: 'pending' | 'ready' | 'missing' | 'invalid' | 'not-a-repo'; legacy: boolean; degraded: boolean; attentionReason?: 'merge' | 'rebase' | 'cherry-pick' | 'revert' | 'bisect' | null; }> { const runtime = getRuntimeGit(); if (runtime?.canonicalizeWorktreeState) { return runtime.canonicalizeWorktreeState(directory); } return gitHttp.canonicalizeWorktreeState(directory); }