import { useCallback, useMemo, type ReactNode } from "react"; import { Text, View } from "react-native"; import { useTranslation } from "react-i18next"; import { FileDiff, GitCommitHorizontal } from "lucide-react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import invariant from "tiny-invariant"; import { useRetainedPanelActive } from "@/components/retained-panel"; import { useIsCompactFormFactor } from "@/constants/layout"; import { PaneContentToolbar } from "@/components/ui/pane-content-toolbar"; import { isWeb } from "@/constants/platform"; import { DiffDocument } from "@/git/diff-document"; import { ChangesSurface, DiffLayoutToggle, resolveDiffLayout } from "@/git/diff-pane"; import { useCommitDiffFiles } from "@/git/use-diff-files"; import { useChangesPreferences } from "@/hooks/use-changes-preferences"; import { useAppSettings } from "@/hooks/use-settings"; import { usePaneContext } from "@/panels/pane-context"; import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry"; import { useAddFileToChat } from "@/panels/use-add-file-to-chat"; import { useWorkspaceDirectory } from "@/stores/session-store-hooks"; import type { WorkspaceTabTarget } from "@/workspace-tabs/model"; import { defaultChangesState, changesStateSchema } from "@/panels/changes/state"; import { usePanelState } from "@/panels/use-panel-state"; const ThemedFileDiff = withUnistyles(FileDiff); const ThemedGitCommitHorizontal = withUnistyles(GitCommitHorizontal); function useDiffPanelPreferences() { const { settings } = useAppSettings(); const { preferences, updatePreferences } = useChangesPreferences(); const isCompact = useIsCompactFormFactor(); const canUseSplitLayout = isWeb && !isCompact; const effectiveLayout = resolveDiffLayout(preferences.layout, canUseSplitLayout); const displayPreferences = useMemo( () => ({ layout: effectiveLayout, wrapLines: preferences.wrapLines, codeFontSize: settings.codeFontSize, monoFontFamily: settings.monoFontFamily, }), [effectiveLayout, preferences.wrapLines, settings.codeFontSize, settings.monoFontFamily], ); const toggleLayout = useCallback(() => { void updatePreferences({ layout: preferences.layout === "unified" ? "split" : "unified" }); }, [preferences.layout, updatePreferences]); const toggleWrapLines = useCallback(() => { void updatePreferences({ wrapLines: !preferences.wrapLines }); }, [preferences.wrapLines, updatePreferences]); const toggleHideWhitespace = useCallback(() => { void updatePreferences({ hideWhitespace: !preferences.hideWhitespace }); }, [preferences.hideWhitespace, updatePreferences]); return { preferences, isCompact, canUseSplitLayout, displayPreferences, toggleLayout, toggleWrapLines, toggleHideWhitespace, }; } function PanelState({ message, tone = "muted", testID, }: { message: string; tone?: "muted" | "error"; testID?: string; }) { return ( {message} ); } function WorkingDiffPanel() { const { t } = useTranslation(); const { serverId, workspaceId, tabId, target, openFileInWorkspace } = usePaneContext(); const [changesState, setChangesState] = usePanelState(changesStateSchema, defaultChangesState); const cwd = useWorkspaceDirectory(serverId, workspaceId); const isActive = useRetainedPanelActive(); const { addFile, canAddToChat } = useAddFileToChat({ serverId, workspaceId }); invariant(target.kind === "working_diff", "WorkingDiffPanel requires working_diff target"); const handleOpenFile = useCallback( (path: string) => openFileInWorkspace({ location: { path }, disposition: "side" }), [openFileInWorkspace], ); if (!cwd) { return ; } return ( ); } function CommitDiffPanel() { const { t } = useTranslation(); const { serverId, workspaceId, target } = usePaneContext(); const cwd = useWorkspaceDirectory(serverId, workspaceId); const panelPreferences = useDiffPanelPreferences(); invariant(target.kind === "commit_diff", "CommitDiffPanel requires commit_diff target"); const { files, isLoading, error, capabilityMissing } = useCommitDiffFiles({ serverId, cwd: cwd ?? "", sha: target.sha, enabled: Boolean(cwd), }); const mode = useMemo(() => ({ kind: "commit" as const }), []); let body: ReactNode; if (!cwd) { body = ; } else if (capabilityMissing) { body = ( ); } else if (error) { body = ( ); } else if (isLoading && files.length === 0) { body = ; } else if (files.length === 0) { body = ; } else { body = ( ); } return ( {panelPreferences.canUseSplitLayout ? ( ) : null} {body} ); } function useWorkingDiffPanelDescriptor(): PanelDescriptor { const { t } = useTranslation(); return { label: t("panels.diff.changesLabel"), subtitle: t("panels.diff.changesSubtitle"), tooltip: t("panels.diff.changesSubtitle"), titleState: "ready", icon: ThemedFileDiff, statusBucket: null, }; } function useCommitDiffPanelDescriptor( target: Extract, ): PanelDescriptor { const { t } = useTranslation(); return { label: target.sha.slice(0, 7), subtitle: t("panels.diff.commitSubtitle"), tooltip: target.sha, titleState: "ready", icon: ThemedGitCommitHorizontal, statusBucket: null, }; } export const workingDiffPanelRegistration: PanelRegistration<"working_diff"> = { kind: "working_diff", component: WorkingDiffPanel, useDescriptor: useWorkingDiffPanelDescriptor, resourceKey: () => "working_diff", }; export const commitDiffPanelRegistration: PanelRegistration<"commit_diff"> = { kind: "commit_diff", component: CommitDiffPanel, useDescriptor: useCommitDiffPanelDescriptor, resourceKey: (target) => target.sha, }; const styles = StyleSheet.create((theme) => ({ container: { flex: 1, minHeight: 0, }, toolbar: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: theme.spacing[2], paddingRight: theme.spacing[2], }, toolbarActions: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: theme.spacing[1], }, body: { flex: 1, minHeight: 0, }, centerState: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: theme.spacing[6], paddingTop: theme.spacing[16], }, mutedText: { fontSize: theme.fontSize.base, color: theme.colors.foregroundMuted, textAlign: "center", }, errorText: { fontSize: theme.fontSize.base, color: theme.colors.destructive, textAlign: "center", }, }));