diff --git a/assets/github.svg b/assets/github.svg
new file mode 100644
index 0000000..5d720c8
--- /dev/null
+++ b/assets/github.svg
@@ -0,0 +1,3 @@
+
diff --git a/package.json b/package.json
index 491a221..d416ebc 100644
--- a/package.json
+++ b/package.json
@@ -12,14 +12,17 @@
"test": "jest --passWithNoTests"
},
"dependencies": {
+ "@octokit/rest": "^22.0.0",
"acorn": "^8.14.0",
"acorn-walk": "^8.3.4",
"d3-array": "^3.2.4",
"escodegen": "^2.1.0",
+ "framer-motion": "^12.23.12",
"plasmo": "0.89.4",
"react": "18.2.0",
"react-dom": "18.2.0",
- "tailwindcss": "3.4.1"
+ "tailwindcss": "3.4.1",
+ "uuid": "^11.1.0"
},
"devDependencies": {
"@babel/preset-env": "^7.26.9",
@@ -35,6 +38,7 @@
"@types/node": "20.11.5",
"@types/react": "18.2.48",
"@types/react-dom": "18.2.18",
+ "@types/uuid": "^10.0.0",
"autoprefixer": "^10.4.20",
"babel-jest": "^29.7.0",
"husky": "^9.1.7",
diff --git a/plasmo.config.ts b/plasmo.config.ts
index e812a69..8e344c6 100644
--- a/plasmo.config.ts
+++ b/plasmo.config.ts
@@ -4,6 +4,10 @@ export default {
"storage",
"cookies",
"webRequest"
+ ],
+ host_permissions: [
+ "https://api.github.com/*",
+ "https://github.com/*"
]
}
}
\ No newline at end of file
diff --git a/src/components/DialogPanel.tsx b/src/components/DialogPanel.tsx
new file mode 100644
index 0000000..a0468eb
--- /dev/null
+++ b/src/components/DialogPanel.tsx
@@ -0,0 +1,306 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { motion } from "framer-motion";
+import faviconIco from "data-base64:../../assets/icon.png";
+
+// Close button component
+const CloseButton = ({ close }: { close: () => void }) => {
+ return (
+
+ ×
+
+ );
+};
+
+// Arrow head component for resize handles
+const ArrowHead = ({ direction }: { direction: 'top' | 'right' | 'bottom' | 'left' }) => {
+ const getArrowStyle = () => {
+ switch (direction) {
+ case 'top':
+ return { transform: 'rotate(0deg)' };
+ case 'right':
+ return { transform: 'rotate(90deg)' };
+ case 'bottom':
+ return { transform: 'rotate(180deg)' };
+ case 'left':
+ return { transform: 'rotate(270deg)' };
+ }
+ };
+
+ return (
+
+ );
+};
+
+// Dialog panel component with drag and resize functionality
+export const DialogPanel = ({
+ children,
+ overlay,
+ close
+}: {
+ children: React.ReactNode,
+ overlay?: React.ReactNode,
+ close?: () => void
+}) => {
+ const [panelSize, setPanelSize] = useState<{ width: number; height: number }>({ width: 550, height: 330 });
+ const resizingRef = useRef<{
+ startX: number;
+ startY: number;
+ startW: number;
+ startH: number;
+ startLeft: number;
+ startTop: number;
+ viewportW: number;
+ viewportH: number;
+ edge: 'top'|'right'|'bottom'|'left';
+ } | null>(null);
+
+ const [pos, setPos] = useState<{ top: number; left: number }>(() => {
+ const minMargin = 4;
+ const bottom = 130;
+ const right = 80;
+ const top = Math.max(minMargin, window.innerHeight - bottom - 365);
+ const left = Math.max(minMargin, window.innerWidth - right - 550);
+ return { top, left };
+ });
+
+ const draggingRef = useRef<{ startX: number; startY: number; startTop: number; startLeft: number } | null>(null);
+
+ const onMouseMove = useCallback((e: MouseEvent) => {
+ if (!resizingRef.current) return;
+ const dx = e.clientX - resizingRef.current.startX;
+ const dy = e.clientY - resizingRef.current.startY;
+
+ const minW = 320;
+ const minH = 200;
+ const maxW = Math.min(window.innerWidth * 0.92, 900);
+ const maxH = Math.min(window.innerHeight * 0.7, 800);
+
+ let newW = resizingRef.current.startW;
+ let newH = resizingRef.current.startH;
+ let newLeft = pos.left;
+ let newTop = pos.top;
+
+ switch (resizingRef.current.edge) {
+ case 'right':
+ newW = resizingRef.current.startW + dx;
+ break;
+ case 'left':
+ newW = resizingRef.current.startW - dx;
+ newLeft = resizingRef.current.startLeft + dx;
+ break;
+ case 'bottom':
+ newH = resizingRef.current.startH + dy;
+ break;
+ case 'top':
+ newH = resizingRef.current.startH - dy;
+ newTop = resizingRef.current.startTop + dy;
+ break;
+ }
+
+ // Apply constraints
+ newW = Math.max(minW, Math.min(maxW, newW));
+ newH = Math.max(minH, Math.min(maxH, newH));
+ newLeft = Math.max(0, Math.min(window.innerWidth - newW, newLeft));
+ newTop = Math.max(0, Math.min(window.innerHeight - newH, newTop));
+
+ setPanelSize({ width: newW, height: newH });
+ if (newLeft !== pos.left || newTop !== pos.top) {
+ setPos({ left: newLeft, top: newTop });
+ }
+ }, [panelSize.width, panelSize.height, pos.left, pos.top]);
+
+ const onMouseMoveDrag = useCallback((e: MouseEvent) => {
+ if (!draggingRef.current) return;
+ const dx = e.clientX - draggingRef.current.startX;
+ const dy = e.clientY - draggingRef.current.startY;
+
+ const newLeft = draggingRef.current.startLeft + dx;
+ const newTop = draggingRef.current.startTop + dy;
+
+ const minMargin = 4;
+ const maxLeft = window.innerWidth - panelSize.width - minMargin;
+ const maxTop = window.innerHeight - panelSize.height - minMargin;
+
+ setPos({
+ left: Math.max(minMargin, Math.min(maxLeft, newLeft)),
+ top: Math.max(minMargin, Math.min(maxTop, newTop))
+ });
+ }, [panelSize.width, panelSize.height]);
+
+ const endResize = useCallback(() => {
+ resizingRef.current = null;
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+
+ // Remove event listeners
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('mouseup', endResize);
+ }, [onMouseMove]);
+
+ const endDrag = useCallback(() => {
+ draggingRef.current = null;
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+
+ // Remove event listeners
+ document.removeEventListener('mousemove', onMouseMoveDrag);
+ document.removeEventListener('mouseup', endDrag);
+ }, [onMouseMoveDrag]);
+
+ const startResize = useCallback((e: React.MouseEvent, edge: 'top'|'right'|'bottom'|'left') => {
+ e.preventDefault();
+ resizingRef.current = {
+ startX: e.clientX,
+ startY: e.clientY,
+ startW: panelSize.width,
+ startH: panelSize.height,
+ startLeft: pos.left,
+ startTop: pos.top,
+ viewportW: window.innerWidth,
+ viewportH: window.innerHeight,
+ edge
+ };
+ document.body.style.cursor = edge === 'left' || edge === 'right' ? 'ew-resize' : 'ns-resize';
+ document.body.style.userSelect = 'none';
+
+ // Add event listeners
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('mouseup', endResize);
+ }, [panelSize.width, panelSize.height, pos.left, pos.top, onMouseMove, endResize]);
+
+ const startDrag = useCallback((e: React.MouseEvent) => {
+ e.preventDefault();
+ draggingRef.current = {
+ startX: e.clientX,
+ startY: e.clientY,
+ startTop: pos.top,
+ startLeft: pos.left
+ };
+ document.body.style.cursor = 'grabbing';
+ document.body.style.userSelect = 'none';
+
+ // Add event listeners
+ document.addEventListener('mousemove', onMouseMoveDrag);
+ document.addEventListener('mouseup', endDrag);
+ }, [pos.top, pos.left, onMouseMoveDrag, endDrag]);
+
+ useEffect(() => {
+ const handleResize = () => {
+ const minMargin = 4;
+ const bottom = 130;
+ const right = 80;
+ const newTop = Math.max(minMargin, window.innerHeight - bottom - 365);
+ const newLeft = Math.max(minMargin, window.innerWidth - right - 550);
+
+ if (newLeft !== pos.left || newTop !== pos.top) {
+ setPos({ left: newLeft, top: newTop });
+ }
+ };
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, [panelSize.width, panelSize.height, pos.left, pos.top]);
+
+ useEffect(() => () => endDrag(), [endDrag]);
+
+ return (
+
+ {close && }
+
+
+
+
+
+
+ Mantis
+
+
+
+ {children}
+
+
+
+ {/* Resize handles */}
+ startResize(e, 'top')}
+ className="absolute top-0 left-0 right-0 h-2 cursor-n-resize group"
+ style={{ transform: 'translateY(-1px)' }}
+ title="Resize"
+ >
+
+
+ startResize(e, 'bottom')}
+ className="absolute bottom-0 left-0 right-0 h-2 cursor-s-resize group"
+ style={{ transform: 'translateY(1px)' }}
+ title="Resize"
+ >
+
+
+ startResize(e, 'left')}
+ className="absolute left-0 top-0 bottom-0 w-2 cursor-w-resize group"
+ style={{ transform: 'translateX(-1px)' }}
+ title="Resize"
+ >
+
+
+ startResize(e, 'right')}
+ className="absolute right-0 top-0 bottom-0 w-2 cursor-e-resize group"
+ style={{ transform: 'translateX(1px)' }}
+ title="Resize"
+ >
+
+
+
+ {overlay && (
+
+ {overlay}
+
+ )}
+
+ );
+};
diff --git a/src/connection_manager.tsx b/src/connection_manager.tsx
index 2e48d7a..164b69d 100644
--- a/src/connection_manager.tsx
+++ b/src/connection_manager.tsx
@@ -6,9 +6,10 @@ import { GoogleScholarConnection } from "./connections/googleScholar/connection"
import { WikipediaSegmentConnection } from "./connections/wikipediaSegment/connection";
import { GmailConnection } from "./connections/Gmail/connection";
import { LinkedInConnection } from "./connections/Linkedin/connection";
+import { GitBlameConnection } from "./connections/gitblame/connection";
-export const CONNECTIONS = [GmailConnection, WikipediaSegmentConnection, WikipediaReferencesConnection, GoogleConnection, PubmedConnection, GoogleDocsConnection, GoogleScholarConnection,LinkedInConnection];
+export const CONNECTIONS = [GmailConnection, WikipediaSegmentConnection, WikipediaReferencesConnection, GoogleConnection, PubmedConnection, GoogleDocsConnection, GoogleScholarConnection, LinkedInConnection, GitBlameConnection];
export const searchConnections = (url: string, ) => {
const connections = CONNECTIONS.filter(connection => connection.trigger(url));
diff --git a/src/connections/gitblame/connection.tsx b/src/connections/gitblame/connection.tsx
new file mode 100644
index 0000000..9b3048c
--- /dev/null
+++ b/src/connections/gitblame/connection.tsx
@@ -0,0 +1,638 @@
+import type { MantisConnection, injectUIType, onMessageType, registerListenersType, setProgressType, establishLogSocketType } from "../types";
+import { GenerationProgress } from "../types";
+import { Octokit } from "@octokit/rest";
+import { saveGitHubToken, getGitHubToken } from "../../github-token-manager";
+
+import githubIcon from "data-base64:../../../assets/github.svg";
+import { getSpacePortal, registerAuthCookies, reqSpaceCreation } from "../../driver";
+
+// Define types for better type safety
+type BlameMapEntry = {
+ filename: string;
+ lineNumber: number;
+ commit: string;
+ author?: string;
+ date?: string;
+ lineContent: string;
+};
+
+type GitHubBlameRange = {
+ startingLine: number;
+ endingLine: number;
+ age: number;
+ commit: {
+ oid: string;
+ author: {
+ name: string;
+ date: string;
+ };
+ };
+};
+
+const trigger = (url: string) => {
+ return url.includes("github.com") && (url.includes("/blob/") || url.includes("/pull/"));
+}
+
+// Check if a repository is public (no authentication required)
+async function isRepositoryPublic(owner: string, repo: string): Promise {
+ try {
+ // Try to access the repository without authentication
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
+
+ // Check if we got a valid JSON response
+ const contentType = response.headers.get('content-type');
+ if (!contentType || !contentType.includes('application/json')) {
+ console.warn('GitHub API returned non-JSON response, assuming private repository');
+ return false;
+ }
+
+ if (response.status === 200) {
+ try {
+ const data = await response.json();
+ // Check if the repository is actually public
+ return !data.private;
+ } catch (jsonError) {
+ console.warn('Failed to parse JSON response:', jsonError);
+ return false;
+ }
+ }
+
+ // If we get a 404, the repo might not exist or be private
+ if (response.status === 404) {
+ console.warn('Repository not found, assuming private');
+ return false;
+ }
+
+ // If we get rate limited or other errors, assume private
+ if (response.status === 403 || response.status === 429) {
+ console.warn('GitHub API rate limited or forbidden, assuming private');
+ return false;
+ }
+
+ return false;
+ } catch (error) {
+ console.warn('Could not determine repository visibility:', error);
+ return false; // Assume private if we can't determine
+ }
+}
+
+// Add this function to handle token input
+async function promptForGitHubToken(): Promise {
+ return new Promise((resolve) => {
+ const token = prompt(
+ "Please enter your GitHub Personal Access Token:\n\n" +
+ "This token will be stored locally in your browser and used to access GitHub's API.\n" +
+ "You can create a token at: https://github.com/settings/tokens\n\n" +
+ "Required permissions: repo (for private repos) or public_repo (for public repos only)",
+ ""
+ );
+
+ if (token && token.trim()) {
+ resolve(token.trim());
+ } else {
+ resolve("");
+ }
+ });
+}
+
+// Add this function to validate token
+async function validateGitHubToken(token: string): Promise {
+ try {
+ const octokit = new Octokit({ auth: token });
+ const { data } = await octokit.rest.users.getAuthenticated();
+ return !!data.login;
+ } catch (error) {
+ console.warn('Invalid GitHub token:', error);
+ return false;
+ }
+}
+
+const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, onMessage: onMessageType, registerListeners: registerListenersType, establishLogSocket: establishLogSocketType) => {
+ setProgress(GenerationProgress.GATHERING_DATA);
+
+ // Extract repository information from the URL more robustly
+ const url = new URL(window.location.href);
+ const pathParts = url.pathname.split('/');
+ const owner = pathParts[1];
+ const repo = pathParts[2];
+
+ // More robust branch and file path extraction
+ let branch = "main";
+ let filePath = "";
+
+ // Check if this is a PR page
+ const isPR = pathParts.includes('pull');
+ const prIndex = pathParts.indexOf('pull');
+
+ if (isPR && prIndex !== -1 && prIndex + 1 < pathParts.length) {
+ // This is a PR page - extract PR number and get the head branch
+ const prNumber = pathParts[prIndex + 1];
+ console.log(`Processing PR #${prNumber} for ${owner}/${repo}`);
+
+ // For PR pages, we need to get the head branch from the PR data
+ // We'll try to extract it from the page or use a fallback
+ const prBranchMeta = document.querySelector('meta[name="pr-head-branch"]');
+ if (prBranchMeta) {
+ branch = prBranchMeta.getAttribute('content') || "main";
+ } else {
+ // Fallback: try to get branch from the page content
+ const branchElement = document.querySelector('[data-testid="head-ref"]') ||
+ document.querySelector('.head-ref') ||
+ document.querySelector('[title*="head"]');
+ if (branchElement) {
+ branch = branchElement.textContent?.trim() || "main";
+ }
+ }
+
+ // For PR pages, we need to determine the file path differently
+ // Check if we're viewing a specific file in the PR
+ const filePathMeta = document.querySelector('meta[name="file-path"]');
+ if (filePathMeta) {
+ filePath = filePathMeta.getAttribute('content') || "";
+ } else {
+ // Try to extract from URL if it's a file view in PR
+ const filesIndex = pathParts.indexOf('files');
+ if (filesIndex !== -1 && filesIndex + 1 < pathParts.length) {
+ filePath = pathParts.slice(filesIndex + 1).join('/');
+ }
+ }
+ } else {
+ // This is a regular blob page - use existing logic
+ // Look for branch name in meta tags first (more reliable)
+ const branchMeta = document.querySelector('meta[name="branch-name"]');
+ if (branchMeta) {
+ branch = branchMeta.getAttribute('content') || "main";
+ } else {
+ // Fallback to URL parsing, but handle branch names with slashes more intelligently
+ const blobIndex = pathParts.indexOf('blob');
+ if (blobIndex !== -1 && blobIndex + 1 < pathParts.length) {
+ const afterBlob = pathParts.slice(blobIndex + 1);
+
+ if (afterBlob.length >= 2) {
+ // For better branch detection, we need to be smarter about where the branch ends
+ // GitHub URLs typically have the pattern: /owner/repo/blob/branch/path/to/file
+ // But branch names can contain slashes, so we need to find the right split point
+
+ // Try to find the file extension to determine where the file path starts
+ let filePathStartIndex = 0;
+ for (let i = 0; i < afterBlob.length; i++) {
+ const part = afterBlob[i];
+ // If this part contains a file extension, it's likely part of the file path
+ if (part.includes('.') && !part.includes('/')) {
+ filePathStartIndex = i;
+ break;
+ }
+ // If this part looks like a commit hash (40+ hex chars), it's likely a commit, not a branch
+ if (/^[a-f0-9]{40,}$/.test(part)) {
+ filePathStartIndex = i;
+ break;
+ }
+ }
+
+ if (filePathStartIndex > 0) {
+ // We found a likely file path start, everything before is the branch
+ branch = afterBlob.slice(0, filePathStartIndex).join('/');
+ filePath = afterBlob.slice(filePathStartIndex).join('/');
+ } else {
+ // Fallback: assume first part is branch, rest is file path
+ // This handles cases like "feature/new-feature" as branch name
+ branch = afterBlob[0];
+ filePath = afterBlob.slice(1).join('/');
+ }
+ } else if (afterBlob.length === 1) {
+ // Only one part after blob, assume it's the branch
+ branch = afterBlob[0];
+ filePath = "";
+ }
+ }
+ }
+ }
+
+ // If we still don't have a file path, try to extract it from the page
+ if (!filePath) {
+ const filePathMeta = document.querySelector('meta[name="file-path"]');
+ if (filePathMeta) {
+ filePath = filePathMeta.getAttribute('content') || "";
+ }
+ }
+
+ console.log(`Processing repository: ${owner}/${repo}, branch: ${branch}, file: ${filePath}`);
+
+ // Check if repository is public first
+ const isPublic = await isRepositoryPublic(owner, repo);
+
+ let githubToken: string | undefined;
+ let octokit: Octokit;
+
+ if (isPublic) {
+ // For public repos, we can work without authentication
+ console.log('Repository is public, proceeding without authentication');
+ octokit = new Octokit();
+
+ // However, some GitHub API operations (like GraphQL) may still require authentication
+ // We'll try without auth first, but fall back to asking for a token if needed
+ } else {
+ // For private repos, we need authentication
+ console.log('Repository is private, authentication required');
+
+ // Check if user has a GitHub token
+ githubToken = await getGitHubToken();
+
+ if (!githubToken) {
+ // Prompt user for token
+ githubToken = await promptForGitHubToken();
+
+ if (!githubToken) {
+ throw new Error('GitHub Personal Access Token is required for private repositories.');
+ }
+
+ // Validate the token
+ const isValid = await validateGitHubToken(githubToken);
+ if (!isValid) {
+ throw new Error('Invalid GitHub Personal Access Token. Please check your token and try again.');
+ }
+
+ // Save the valid token
+ await saveGitHubToken(githubToken);
+ }
+
+ octokit = new Octokit({ auth: githubToken });
+ }
+
+ try {
+ // Get file blame information with better error handling
+ let blameData: BlameMapEntry[] = [];
+
+ try {
+ blameData = await getFileBlame(octokit, owner, repo, filePath, branch);
+ } catch (blameError) {
+ console.warn('Failed to get blame data:', blameError);
+
+ // If we get JSON parsing errors, it's likely an authentication issue
+ if (blameError.message && (blameError.message.includes('Unexpected token') || blameError.message.includes(''))) {
+ console.log('Detected authentication issue, prompting for token...');
+
+ // Ask for a token
+ const fallbackToken = await promptForGitHubToken();
+ if (fallbackToken) {
+ const isValid = await validateGitHubToken(fallbackToken);
+ if (isValid) {
+ await saveGitHubToken(fallbackToken);
+ const authenticatedOctokit = new Octokit({ auth: fallbackToken });
+
+ try {
+ blameData = await getFileBlame(authenticatedOctokit, owner, repo, filePath, branch);
+ console.log('Successfully retrieved blame data with authentication');
+ } catch (retryError) {
+ console.error('Failed to get blame data even with authentication:', retryError);
+ throw new Error('Unable to retrieve blame data. Please check your GitHub token permissions.');
+ }
+ } else {
+ throw new Error('Invalid GitHub token. Please check your token and try again.');
+ }
+ } else {
+ throw new Error('GitHub Personal Access Token is required for this repository.');
+ }
+ } else {
+ throw blameError;
+ }
+ }
+
+ // If we still have no blame data, try one more time with authentication
+ if (blameData.length === 0 && !githubToken) {
+ console.log('No blame data received, this might require authentication. Prompting for token...');
+
+ // Ask for a token even for public repos if GraphQL operations fail
+ const fallbackToken = await promptForGitHubToken();
+ if (fallbackToken) {
+ const isValid = await validateGitHubToken(fallbackToken);
+ if (isValid) {
+ await saveGitHubToken(fallbackToken);
+ const authenticatedOctokit = new Octokit({ auth: fallbackToken });
+
+ try {
+ const retryBlameData = await getFileBlame(authenticatedOctokit, owner, repo, filePath, branch);
+
+ if (retryBlameData.length > 0) {
+ console.log('Successfully retrieved blame data with authentication');
+ blameData = retryBlameData;
+ }
+ } catch (retryError) {
+ console.error('Failed to get blame data even with authentication:', retryError);
+ throw new Error('Unable to retrieve blame data. Please check your GitHub token permissions.');
+ }
+ } else {
+ throw new Error('Invalid GitHub token. Please check your token and try again.');
+ }
+ } else {
+ throw new Error('GitHub Personal Access Token is required for this repository.');
+ }
+ }
+
+ // Get additional repository information
+ const repoInfo = await getRepositoryInfo(octokit, owner, repo);
+
+ // Combine data for space creation
+ const extractedData = blameData.map(entry => ({
+ filename: entry.filename,
+ lineNumber: entry.lineNumber,
+ commit: entry.commit,
+ author: entry.author,
+ date: entry.date,
+ lineContent: entry.lineContent,
+ repository: `${owner}/${repo}`,
+ branch: branch,
+ isPR: isPR,
+ prNumber: isPR ? pathParts[prIndex + 1] : undefined
+ }));
+
+ // Add repository metadata
+ if (repoInfo) {
+ extractedData.push({
+ filename: "repository_info",
+ lineNumber: 0,
+ commit: "metadata",
+ author: repoInfo.owner.login,
+ date: repoInfo.created_at,
+ lineContent: `Repository: ${repoInfo.full_name}, Description: ${repoInfo.description || 'No description'}, Language: ${repoInfo.language || 'Unknown'}`,
+ repository: `${owner}/${repo}`,
+ branch: branch,
+ isPR: isPR,
+ prNumber: isPR ? pathParts[prIndex + 1] : undefined
+ });
+ }
+
+ console.log(`Extracted ${extractedData.length} blame entries`);
+
+ setProgress(GenerationProgress.CREATING_SPACE);
+
+ const spaceData = await reqSpaceCreation(extractedData, {
+ "filename": "text",
+ "lineNumber": "number",
+ "commit": "text",
+ "author": "text",
+ "date": "date",
+ "lineContent": "semantic",
+ "repository": "text",
+ "branch": "text",
+ "isPR": "boolean",
+ "prNumber": "text"
+ }, establishLogSocket, `GitBlame: ${owner}/${repo}/${filePath}${isPR ? ` (PR #${pathParts[prIndex + 1]})` : ''}`);
+
+ setProgress(GenerationProgress.INJECTING_UI);
+
+ const spaceId = spaceData.space_id;
+ const createdWidget = await injectUI(spaceId, onMessage, registerListeners);
+
+ setProgress(GenerationProgress.COMPLETED);
+
+ return { spaceId, createdWidget };
+
+ } catch (error) {
+ console.error('Error creating GitBlame space:', error);
+
+ // Provide more helpful error messages
+ if (error.message && error.message.includes('Unexpected token')) {
+ throw new Error('GitHub API returned an invalid response. This usually means authentication is required. Please provide a valid GitHub Personal Access Token.');
+ }
+
+ if (error.message && error.message.includes('')) {
+ throw new Error('GitHub API returned an HTML error page. This usually means rate limiting or authentication issues. Please try again later or provide a valid GitHub Personal Access Token.');
+ }
+
+ throw error;
+ }
+}
+
+async function getFileBlame(octokit: Octokit, owner: string, repo: string, path: string, branch: string): Promise {
+ try {
+ // Use GitHub's GraphQL API for proper blame data
+ const query = `
+ query($owner: String!, $repo: String!, $path: String!, $ref: String!) {
+ repository(owner: $owner, name: $repo) {
+ object(expression: $ref) {
+ ... on Commit {
+ blame(path: $path) {
+ ranges {
+ startingLine
+ endingLine
+ age
+ commit {
+ oid
+ author {
+ name
+ date
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ owner,
+ repo,
+ path,
+ ref: branch
+ };
+
+ // Make GraphQL request and get file content in parallel
+ const [response, fileContentResponse] = await Promise.all([
+ octokit.graphql(query, variables),
+ octokit.rest.repos.getContent({ owner, repo, path, ref: branch })
+ ]);
+
+ // Check if we got HTML error pages instead of JSON
+ if (typeof response === 'string' && response.includes('')) {
+ throw new Error('GitHub API returned an HTML error page. This usually means rate limiting or authentication issues.');
+ }
+
+ // Check if the response is valid JSON
+ if (typeof response === 'string') {
+ try {
+ JSON.parse(response);
+ } catch (jsonError) {
+ throw new Error('GitHub API returned invalid JSON response. This usually means authentication is required.');
+ }
+ }
+
+ // Type the GraphQL response properly and validate it's not HTML
+ const typedResponse = response as any;
+
+ // Check if we got a valid response structure
+ if (!typedResponse || typeof typedResponse !== 'object') {
+ console.warn('Invalid GraphQL response structure:', typedResponse);
+ return [];
+ }
+
+ if (!typedResponse.repository || !typedResponse.repository.object) {
+ console.warn('Repository or object not found in GraphQL response');
+ return [];
+ }
+
+ const blameData = typedResponse.repository.object.blame?.ranges || [];
+
+ if (Array.isArray(fileContentResponse.data)) {
+ // This shouldn't happen for a file path, but handle it
+ return [];
+ }
+
+ // Check if it's a file (not a symlink or submodule)
+ if (fileContentResponse.data.type !== 'file') {
+ console.warn(`Path ${path} is not a file (type: ${fileContentResponse.data.type})`);
+ return [];
+ }
+
+ // Check if we got HTML error pages instead of file content
+ if (typeof fileContentResponse.data === 'string' && (fileContentResponse.data as string).includes('')) {
+ throw new Error('GitHub API returned an HTML error page when fetching file content. This usually means rate limiting or authentication issues.');
+ }
+
+ // Check if the file content response is valid
+ if (!fileContentResponse.data || !fileContentResponse.data.content) {
+ throw new Error('GitHub API returned invalid file content response. This usually means authentication is required.');
+ }
+
+ const content = Buffer.from(fileContentResponse.data.content, 'base64').toString('utf-8');
+ const lines = content.split('\n');
+
+ // Convert GraphQL response to our format
+ const result: BlameMapEntry[] = [];
+
+ for (const range of blameData) {
+ const { startingLine, endingLine, commit } = range;
+
+ // Add each line in the range
+ for (let lineNum = startingLine; lineNum <= endingLine; lineNum++) {
+ if (lineNum > 0 && lineNum <= lines.length) {
+ result.push({
+ filename: path,
+ lineNumber: lineNum,
+ commit: commit.oid,
+ author: commit.author?.name,
+ date: commit.author?.date,
+ lineContent: lines[lineNum - 1] || ''
+ });
+ }
+ }
+ }
+
+ // Sort by line number
+ return result.sort((a, b) => a.lineNumber - b.lineNumber);
+
+ } catch (error) {
+ console.warn(`Could not get blame for ${path}:`, error);
+
+ // Check if the error is due to authentication issues
+ if (error.message && error.message.includes('Unexpected token')) {
+ console.warn('This appears to be an authentication issue. Please provide a valid GitHub token.');
+ }
+
+ if (error.message && error.message.includes('')) {
+ console.warn('GitHub API returned an HTML error page. This usually means rate limiting or authentication issues.');
+ }
+
+ return [];
+ }
+}
+
+async function getRepositoryInfo(octokit: Octokit, owner: string, repo: string) {
+ try {
+ const { data } = await octokit.rest.repos.get({
+ owner,
+ repo
+ });
+ return data;
+ } catch (error) {
+ console.warn(`Could not get repository info for ${owner}/${repo}:`, error);
+ return null;
+ }
+}
+
+const injectUI = async (space_id: string, onMessage: onMessageType, registerListeners: registerListenersType) => {
+ // Find the GitHub file header to inject our UI
+ // For PR pages, we need to look in different places
+ const isPR = window.location.href.includes('/pull/');
+
+ let fileHeader;
+ if (isPR) {
+ // For PR pages, look for the file header in the diff view
+ fileHeader = document.querySelector('.file-header') ||
+ document.querySelector('.Box-header') ||
+ document.querySelector('.d-flex.flex-column.flex-md-row') ||
+ document.querySelector('[data-testid="file-header"]') ||
+ document.querySelector('.js-file-header');
+ } else {
+ // For regular blob pages, use the standard selectors
+ fileHeader = document.querySelector('.file-header') ||
+ document.querySelector('.Box-header') ||
+ document.querySelector('.d-flex.flex-column.flex-md-row');
+ }
+
+ if (!fileHeader) {
+ throw new Error('Could not find GitHub file header');
+ }
+
+ // Container for everything
+ const div = document.createElement("div");
+
+ // Toggle switch wrapper
+ const label = document.createElement("label");
+ label.classList.add("inline-flex", "items-center", "cursor-pointer", "ml-4", "mr-4");
+
+ // Checkbox as toggle
+ const checkbox = document.createElement("input");
+ checkbox.type = "checkbox";
+ checkbox.classList.add("hidden");
+
+ // Text container with GitHub-style styling
+ const textContainer = document.createElement("span");
+ textContainer.innerText = isPR ? "Mantis GitBlame (PR)" : "Mantis GitBlame";
+ textContainer.classList.add("font-semibold", "text-sm");
+ // Use CSS custom properties for gradient text since Tailwind doesn't support it
+ textContainer.style.background = "linear-gradient(90deg, #0366d6, #28a745)";
+ textContainer.style.backgroundClip = "text";
+ textContainer.style.webkitTextFillColor = "transparent";
+
+ await registerAuthCookies();
+
+ const iframeScalerParent = await getSpacePortal(space_id, onMessage, registerListeners);
+ iframeScalerParent.classList.add("hidden");
+
+ // Toggle behavior
+ checkbox.addEventListener("change", () => {
+ if (checkbox.checked) {
+ iframeScalerParent.classList.remove("hidden");
+ textContainer.style.background = "linear-gradient(90deg, #28a745, #0366d6)";
+ } else {
+ iframeScalerParent.classList.add("hidden");
+ textContainer.style.background = "linear-gradient(90deg, #0366d6, #28a745)";
+ }
+ textContainer.style.backgroundClip = "text";
+ });
+
+ // Assemble elements
+ label.appendChild(textContainer);
+ label.appendChild(checkbox);
+ div.appendChild(label);
+
+ // Insert the iframe after the file header
+ fileHeader.parentNode?.insertBefore(iframeScalerParent, fileHeader.nextSibling);
+
+ // Insert into the file header
+ fileHeader.appendChild(div);
+
+ return div;
+}
+
+export const GitBlameConnection: MantisConnection = {
+ name: "GitBlame",
+ description: "Builds spaces based on Git blame information from GitHub repositories",
+ icon: githubIcon,
+ trigger: trigger,
+ createSpace: createSpace,
+ injectUI: injectUI,
+}
diff --git a/src/content.tsx b/src/content.tsx
index 82d155f..ac4258a 100644
--- a/src/content.tsx
+++ b/src/content.tsx
@@ -8,6 +8,8 @@ import type { LogMessage, MantisConnection } from "./connections/types";
import { GenerationProgress, Progression } from "./connections/types";
import { addSpaceToCache, deleteSpacesWhere, getCachedSpaces } from "./persistent";
import { refetchAuthCookies } from "./driver";
+import { motion, AnimatePresence} from "framer-motion";
+import { DialogPanel } from "./components/DialogPanel";
export const config: PlasmoCSConfig = {
matches: [""],
@@ -41,31 +43,24 @@ const sanitizeWidget = (widget: HTMLElement, connection: MantisConnection) => {
// Exits the dialog
const CloseButton = ({ close }: { close: () => void }) => {
- return ;
-};
-
-// Dialog util
-const DialogHeader = ({ children }: { children: React.ReactNode }) => {
return (
-
+
+ ×
+
);
-}
+};
// Displays a navigation arrowhead
const ArrowHead = ({ left, disabled }: { left: boolean, disabled: boolean }) => {
return (
// Main dialog that appears when creating a space
const ConnectionDialog = ({ activeConnections, close }: { activeConnections: MantisConnection[], close: () => void }) => {
+ const [showInitialText, setShowInitialText] = useState(true);
const [state, setState] = useState(GenerationProgress.GATHERING_DATA); // Progress of creation process
const [errorText, setErrorText] = useState(null);
const [running, setRunning] = useState(false); // If the creation process is running
@@ -92,6 +88,45 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man
const [logMessages, setLogMessages] = useState([]);
const logContainerRef = useRef(null);
+ const [showOverlay, setShowOverlay] = useState(true);
+
+ const overlayElement = (
+
+ {showOverlay && (
+ setShowOverlay(false)}
+ >
+
+
+
+ Mantis
+
+
+
+ )}
+
+ );
// Check if the log scroll is at the bottom
const isScrolledToBottom = () => {
@@ -246,6 +281,13 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man
const progressPercent = Progression.indexOf(state) / (Progression.length - 1);
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setShowInitialText(false);
+ }, 1000);
+ return () => clearTimeout(timer);
+ }, []);
+
// On opening
useEffect(() => {
// Make sure the user knows that they will be overwriting the existing space on the URL
@@ -276,8 +318,7 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man
if (state === GenerationProgress.COMPLETED) {
return (
-
-
+
{connectionData}
@@ -311,67 +352,107 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man
-
+
);
}
if (state === GenerationProgress.FAILED) {
return (
-
-
- {connectionData}
- {errorText}
-
-
+
+ {connectionData}
+ {errorText}
+
+
);
}
if (running) {
return (
-
- {connectionData}
- {state !== GenerationProgress.CREATING_SPACE ?
- // Raw progress bar with no logs
- ()
-
- // Progress bar + logs
- : (
-
-
- Progress
- {state}
-
-
-
-
-
-
-
Log Messages
-
-
-
{WSStatus}
+
+ {showInitialText ? (
+
+
+ Mantis
+
+
+ ) : (
+
+ {connectionData}
+
+ {state !== GenerationProgress.CREATING_SPACE ? (
+
+
+
Create New Space
+
+
+
+
+
+
+
+
+
+
![{activeConnections[connectionIdx].name}]({activeConnections[connectionIdx].icon})
+
+
+
{activeConnections[connectionIdx].name}
+
{activeConnections[connectionIdx].description}
+
+
+
+
+
+
+
-
+ ) : (
+
+
+
+
Creating Space
+
+ {Progression.indexOf(state) + 1} of {Progression.length} steps
+
+
{logMessages.length === 0 ? (
No log messages yet
@@ -393,16 +474,17 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man
))
)}
-
-
+
+
)}
-
+
+ )}
+
);
}
return (
-
-
+
{activeConnections.length > 1 && (
@@ -454,10 +536,10 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man
onClick={() => runConnection(activeConnection)}
disabled={!isAuthenticated}
>
-
Create
+
Create Space
✨
-
+
);
};
@@ -521,10 +603,10 @@ const PlasmoFloatingButton = () => {
return (
<>
{open && (
setOpen(false)} />
diff --git a/src/github-token-manager.ts b/src/github-token-manager.ts
new file mode 100644
index 0000000..c50f6a8
--- /dev/null
+++ b/src/github-token-manager.ts
@@ -0,0 +1,20 @@
+// Functions to manage GitHub token storage
+export const GITHUB_TOKEN_KEY = 'github_personal_access_token';
+
+export async function saveGitHubToken(token: string): Promise {
+ await chrome.storage.local.set({ [GITHUB_TOKEN_KEY]: token });
+}
+
+export async function getGitHubToken(): Promise {
+ const result = await chrome.storage.local.get([GITHUB_TOKEN_KEY]);
+ return result[GITHUB_TOKEN_KEY] || null;
+}
+
+export async function hasGitHubToken(): Promise {
+ const token = await getGitHubToken();
+ return !!token;
+}
+
+export async function clearGitHubToken(): Promise {
+ await chrome.storage.local.remove([GITHUB_TOKEN_KEY]);
+}
diff --git a/yarn.lock b/yarn.lock
index b4e5688..f989da5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1697,6 +1697,100 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@octokit/auth-token@^6.0.0":
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-6.0.0.tgz#b02e9c08a2d8937df09a2a981f226ad219174c53"
+ integrity sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==
+
+"@octokit/core@^7.0.2":
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/@octokit/core/-/core-7.0.3.tgz#0b5288995fed66920128d41cfeea34979d48a360"
+ integrity sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==
+ dependencies:
+ "@octokit/auth-token" "^6.0.0"
+ "@octokit/graphql" "^9.0.1"
+ "@octokit/request" "^10.0.2"
+ "@octokit/request-error" "^7.0.0"
+ "@octokit/types" "^14.0.0"
+ before-after-hook "^4.0.0"
+ universal-user-agent "^7.0.0"
+
+"@octokit/endpoint@^11.0.0":
+ version "11.0.0"
+ resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-11.0.0.tgz#189fcc022721b4c49d0307eea6be3de1cfb53026"
+ integrity sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==
+ dependencies:
+ "@octokit/types" "^14.0.0"
+ universal-user-agent "^7.0.2"
+
+"@octokit/graphql@^9.0.1":
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-9.0.1.tgz#eb258fc9981403d2d751720832652c385b6c1613"
+ integrity sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==
+ dependencies:
+ "@octokit/request" "^10.0.2"
+ "@octokit/types" "^14.0.0"
+ universal-user-agent "^7.0.0"
+
+"@octokit/openapi-types@^25.1.0":
+ version "25.1.0"
+ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-25.1.0.tgz#5a72a9dfaaba72b5b7db375fd05e90ca90dc9682"
+ integrity sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==
+
+"@octokit/plugin-paginate-rest@^13.0.1":
+ version "13.1.1"
+ resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz#ca5bb1c7b85a583691263c1f788f607e9bcb74b3"
+ integrity sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==
+ dependencies:
+ "@octokit/types" "^14.1.0"
+
+"@octokit/plugin-request-log@^6.0.0":
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz#de1c1e557df6c08adb631bf78264fa741e01b317"
+ integrity sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==
+
+"@octokit/plugin-rest-endpoint-methods@^16.0.0":
+ version "16.0.0"
+ resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.0.0.tgz#ba30ca387fc2ac8bd93cf9f951174736babebd97"
+ integrity sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==
+ dependencies:
+ "@octokit/types" "^14.1.0"
+
+"@octokit/request-error@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-7.0.0.tgz#48ae2cd79008315605d00e83664891a10a5ddb97"
+ integrity sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==
+ dependencies:
+ "@octokit/types" "^14.0.0"
+
+"@octokit/request@^10.0.2":
+ version "10.0.3"
+ resolved "https://registry.yarnpkg.com/@octokit/request/-/request-10.0.3.tgz#2ffdb88105ce20d25dcab8a592a7040ea48306c7"
+ integrity sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==
+ dependencies:
+ "@octokit/endpoint" "^11.0.0"
+ "@octokit/request-error" "^7.0.0"
+ "@octokit/types" "^14.0.0"
+ fast-content-type-parse "^3.0.0"
+ universal-user-agent "^7.0.2"
+
+"@octokit/rest@^22.0.0":
+ version "22.0.0"
+ resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-22.0.0.tgz#9026f47dacba9c605da3d43cce9432c4c532dc5a"
+ integrity sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==
+ dependencies:
+ "@octokit/core" "^7.0.2"
+ "@octokit/plugin-paginate-rest" "^13.0.1"
+ "@octokit/plugin-request-log" "^6.0.0"
+ "@octokit/plugin-rest-endpoint-methods" "^16.0.0"
+
+"@octokit/types@^14.0.0", "@octokit/types@^14.1.0":
+ version "14.1.0"
+ resolved "https://registry.yarnpkg.com/@octokit/types/-/types-14.1.0.tgz#3bf9b3a3e3b5270964a57cc9d98592ed44f840f2"
+ integrity sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==
+ dependencies:
+ "@octokit/openapi-types" "^25.1.0"
+
"@parcel/bundler-default@2.9.3":
version "2.9.3"
resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.9.3.tgz#df18c4b8390a03f83ac6c89da302f9edf48c8fe2"
@@ -3513,6 +3607,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
+"@types/uuid@^10.0.0":
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d"
+ integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==
+
"@types/yargs-parser@*":
version "21.0.3"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
@@ -3988,6 +4087,11 @@ base64-js@^1.3.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+before-after-hook@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-4.0.0.tgz#cf1447ab9160df6a40f3621da64d6ffc36050cb9"
+ integrity sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==
+
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
@@ -4157,15 +4261,10 @@ camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
-caniuse-lite@^1.0.30001541:
- version "1.0.30001704"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz#6644fe909d924ac3a7125e8a0ab6af95b1f32990"
- integrity sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==
-
-caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
- version "1.0.30001703"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz#977cb4920598c158f491ecf4f4f2cfed9e354718"
- integrity sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==
+caniuse-lite@^1.0.30001541, caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
+ version "1.0.30001734"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz"
+ integrity sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==
chai@^5.2.0:
version "5.2.0"
@@ -5083,6 +5182,11 @@ external-editor@^3.1.0:
iconv-lite "^0.4.24"
tmp "^0.0.33"
+fast-content-type-parse@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz#5590b6c807cc598be125e6740a9fde589d2b7afb"
+ integrity sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==
+
fast-fifo@^1.2.0, fast-fifo@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
@@ -5192,6 +5296,15 @@ fraction.js@^4.3.7:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
+framer-motion@^12.23.12:
+ version "12.23.12"
+ resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.23.12.tgz#80cf6fd7c111073a0c558e336c85ca36cca80d3d"
+ integrity sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==
+ dependencies:
+ motion-dom "^12.23.12"
+ motion-utils "^12.23.6"
+ tslib "^2.4.0"
+
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@@ -6848,6 +6961,18 @@ mnemonic-id@3.2.7:
resolved "https://registry.yarnpkg.com/mnemonic-id/-/mnemonic-id-3.2.7.tgz#f7d77d8b39e009ad068117cbafc458a6c6f8cddf"
integrity sha512-kysx9gAGbvrzuFYxKkcRjnsg/NK61ovJOV4F1cHTRl9T5leg+bo6WI0pWIvOFh1Z/yDL0cjA5R3EEGPPLDv/XA==
+motion-dom@^12.23.12:
+ version "12.23.12"
+ resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.23.12.tgz#87974046e7e61bc4932f36d35e8eab6bb6f3e434"
+ integrity sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==
+ dependencies:
+ motion-utils "^12.23.6"
+
+motion-utils@^12.23.6:
+ version "12.23.6"
+ resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.23.6.tgz#fafef80b4ea85122dd0d6c599a0c63d72881f312"
+ integrity sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==
+
ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
@@ -8446,7 +8571,7 @@ ts-node@^10.9.2:
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
-tslib@^2.1.0, tslib@^2.3.0, tslib@^2.8.0:
+tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.8.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@@ -8553,6 +8678,11 @@ unique-string@^3.0.0:
dependencies:
crypto-random-string "^4.0.0"
+universal-user-agent@^7.0.0, universal-user-agent@^7.0.2:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.3.tgz#c05870a58125a2dc00431f2df815a77fe69736be"
+ integrity sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==
+
universalify@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
@@ -8589,6 +8719,11 @@ utility-types@^3.10.0:
resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c"
integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==
+uuid@^11.1.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912"
+ integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==
+
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"