feat: comprehensive service integration refactor with enhanced tabs architecture (#1978)

* feat: add service tabs refactor with GitHub, GitLab, Supabase, Vercel, and Netlify integration

This commit introduces a comprehensive refactor of the connections system,
replacing the single connections tab with dedicated service integration tabs:

 New Service Tabs:
- GitHub Tab: Complete integration with repository management, stats, and API
- GitLab Tab: GitLab project integration and management
- Supabase Tab: Database project management with comprehensive analytics
- Vercel Tab: Project deployment management and monitoring
- Netlify Tab: Site deployment and build management

🔧 Supporting Infrastructure:
- Enhanced store management for each service with auto-connect via env vars
- API routes for secure server-side token handling and data fetching
- Updated TypeScript types with missing properties and interfaces
- Comprehensive hooks for service connections and state management
- Security utilities for API endpoint validation

🎨 UI/UX Improvements:
- Individual service tabs with tailored functionality
- Motion animations and improved loading states
- Connection testing and health monitoring
- Advanced analytics dashboards for each service
- Consistent design patterns across all service tabs

🛠️ Technical Changes:
- Removed legacy connection tab in favor of individual service tabs
- Updated tab configuration and routing system
- Added comprehensive error handling and loading states
- Enhanced type safety with extended interfaces
- Implemented environment variable auto-connection features

Note: Some TypeScript errors remain and will need to be resolved in follow-up commits.
The dev server runs successfully and the service tabs are functional.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: comprehensive service integration refactor with enhanced tabs architecture

Major architectural improvements to service integrations:

**Service Integration Refactor:**
- Complete restructure of service connection tabs (GitHub, GitLab, Vercel, Netlify, Supabase)
- Migrated from centralized ConnectionsTab to dedicated service-specific tabs
- Added shared service integration components for consistent UX
- Implemented auto-connection feature using environment variables

**New Components & Architecture:**
- ServiceIntegrationLayout for consistent service tab structure
- ConnectionStatus, ServiceCard components for reusable UI patterns
- BranchSelector component for repository branch management
- Enhanced authentication dialogs with improved error handling

**API & Backend Enhancements:**
- New API endpoints: github-branches, gitlab-branches, gitlab-projects, vercel-user
- Enhanced GitLab API service with comprehensive project management
- Improved connection testing hooks (useConnectionTest)
- Better error handling and rate limiting across all services

**Configuration & Environment:**
- Updated .env.example with comprehensive service integration guides
- Added auto-connection support for all major services
- Improved development and production environment configurations
- Enhanced tab management with proper service icons

**Code Quality & TypeScript:**
- Fixed all TypeScript errors across service integration components
- Enhanced type definitions for Vercel, Supabase, and other service integrations
- Improved type safety with proper optional chaining and type assertions
- Better separation of concerns between UI and business logic

**Removed Legacy Code:**
- Removed redundant connection components and consolidated into service tabs
- Cleaned up unused imports and deprecated connection patterns
- Streamlined authentication flows across all services

This refactor provides a more maintainable, scalable architecture for service integrations
while significantly improving the user experience for managing external connections.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: clean up dead code and consolidate utilities

- Remove legacy .eslintrc.json (replaced by flat config)
- Remove duplicate app/utils/types.ts (unused type definitions)
- Remove app/utils/cn.ts and consolidate with classNames utility
- Clean up unused ServiceErrorHandler class implementation
- Enhance classNames utility to support boolean values
- Update GlowingEffect.tsx to use consolidated classNames utility

Removes ~150+ lines of unused code while maintaining all functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Simplify terminal health checks and improve project setup

Removed aggressive health checking and reconnection logic from TerminalManager to prevent issues with terminal responsiveness. Updated TerminalTabs to remove onReconnect handlers. Enhanced projectCommands utility to generate non-interactive setup commands and detect shadcn projects, improving automation and reliability of project setup.

* fix: resolve GitLab deployment issues and enhance GitHub deployment reliability

GitLab Deployment Fixes:
- Fix COEP header issue for avatar images by adding crossOrigin and referrerPolicy attributes
- Implement repository name sanitization to handle special characters and ensure GitLab compliance
- Enhance error handling with detailed validation error parsing and user-friendly messages
- Add explicit path field and description to project creation requests
- Improve URL encoding and project path resolution for proper API calls
- Add graceful file commit handling with timeout and error recovery

GitHub Deployment Enhancements:
- Add comprehensive repository name validation and sanitization
- Implement real-time feedback for invalid characters in repository name input
- Enhance error handling with specific error types and retry suggestions
- Improve user experience with better error messages and validation feedback
- Add repository name length limits and character restrictions
- Show sanitized name preview to users before submission

General Improvements:
- Add GitLabAuthDialog component for improved authentication flow
- Enhance logging and debugging capabilities for deployment operations
- Improve accessibility with proper dialog titles and descriptions
- Add better user notifications for name sanitization and validation issues

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Stijnus
2025-09-08 19:29:12 +02:00
committed by GitHub
parent 2fde6f8081
commit 4ca535b9d1
94 changed files with 12201 additions and 2986 deletions

View File

@@ -9,7 +9,7 @@ import type { GitHubUserResponse, GitHubRepoInfo } from '~/types/GitHub';
import { logStore } from '~/lib/stores/logs';
import { chatId } from '~/lib/persistence/useChatHistory';
import { useStore } from '@nanostores/react';
import { AuthDialog as GitHubAuthDialog } from '~/components/@settings/tabs/connections/github/AuthDialog';
import { GitHubAuthDialog } from '~/components/@settings/tabs/github/components/GitHubAuthDialog';
import { SearchInput, EmptyState, StatusIndicator, Badge } from '~/components/ui';
interface GitHubDeploymentDialogProps {
@@ -34,13 +34,33 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
const [showAuthDialog, setShowAuthDialog] = useState(false);
const currentChatId = useStore(chatId);
// Load GitHub connection on mount
/*
* Load GitHub connection on mount
* Helper function to sanitize repository name
*/
const sanitizeRepoName = (name: string): string => {
return (
name
.toLowerCase()
// Replace spaces and underscores with hyphens
.replace(/[\s_]+/g, '-')
// Remove special characters except hyphens and alphanumeric
.replace(/[^a-z0-9-]/g, '')
// Remove multiple consecutive hyphens
.replace(/-+/g, '-')
// Remove leading/trailing hyphens
.replace(/^-+|-+$/g, '')
// Ensure it's not empty and has reasonable length
.substring(0, 100) || 'my-project'
);
};
useEffect(() => {
if (isOpen) {
const connection = getLocalStorage('github_connection');
// Set a default repository name based on the project name
setRepoName(projectName.replace(/\s+/g, '-').toLowerCase());
// Set a default repository name based on the project name with proper sanitization
setRepoName(sanitizeRepoName(projectName));
if (connection?.user && connection?.token) {
setUser(connection.user);
@@ -180,6 +200,25 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
return;
}
// Validate repository name
const sanitizedName = sanitizeRepoName(repoName);
if (!sanitizedName || sanitizedName.length < 1) {
toast.error('Repository name must contain at least one alphanumeric character');
return;
}
if (sanitizedName.length > 100) {
toast.error('Repository name is too long (maximum 100 characters)');
return;
}
// Update the repo name field with the sanitized version if it was changed
if (sanitizedName !== repoName) {
setRepoName(sanitizedName);
toast.info(`Repository name sanitized to: ${sanitizedName}`);
}
setIsLoading(true);
try {
@@ -188,10 +227,11 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
let repoExists = false;
try {
// Check if the repository already exists
// Check if the repository already exists - ensure repo name is properly sanitized
const sanitizedRepoName = sanitizeRepoName(repoName);
const { data: existingRepo } = await octokit.repos.get({
owner: connection.user.login,
repo: repoName,
repo: sanitizedRepoName,
});
repoExists = true;
@@ -219,7 +259,7 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
if (existingRepo.private !== isPrivate) {
await octokit.repos.update({
owner: connection.user.login,
repo: repoName,
repo: sanitizedRepoName,
private: isPrivate,
});
}
@@ -232,8 +272,9 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
// Create repository if it doesn't exist
if (!repoExists) {
const sanitizedRepoName = sanitizeRepoName(repoName);
const { data: newRepo } = await octokit.repos.createForAuthenticatedUser({
name: repoName,
name: sanitizedRepoName,
private: isPrivate,
// Initialize with a README to avoid empty repository issues
@@ -253,7 +294,8 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
await new Promise((resolve) => setTimeout(resolve, 2000));
} else {
// Set URL for existing repo
setCreatedRepoUrl(`https://github.com/${connection.user.login}/${repoName}`);
const sanitizedRepoName = sanitizeRepoName(repoName);
setCreatedRepoUrl(`https://github.com/${connection.user.login}/${sanitizedRepoName}`);
}
// Process files to upload
@@ -279,9 +321,10 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
try {
// For both new and existing repos, get the repository info
const sanitizedRepoName = sanitizeRepoName(repoName);
const { data: repo } = await octokit.repos.get({
owner: connection.user.login,
repo: repoName,
repo: sanitizedRepoName,
});
defaultBranch = repo.default_branch || 'main';
console.log(`Repository default branch: ${defaultBranch}`);
@@ -290,7 +333,7 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
try {
const { data: refData } = await octokit.git.getRef({
owner: connection.user.login,
repo: repoName,
repo: sanitizedRepoName,
ref: `heads/${defaultBranch}`,
});
@@ -300,7 +343,7 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
// Get the latest commit to use as a base for our tree
const { data: commitData } = await octokit.git.getCommit({
owner: connection.user.login,
repo: repoName,
repo: sanitizedRepoName,
commit_sha: baseSha,
});
@@ -331,9 +374,10 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
console.log(`Creating tree with ${tree.length} files using base: ${baseSha || 'none'}`);
// Create a tree with all the files, using the base tree if available
const sanitizedRepoName = sanitizeRepoName(repoName);
const { data: treeData } = await octokit.git.createTree({
owner: connection.user.login,
repo: repoName,
repo: sanitizedRepoName,
tree,
base_tree: baseSha || undefined,
});
@@ -346,7 +390,7 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
try {
const { data: refData } = await octokit.git.getRef({
owner: connection.user.login,
repo: repoName,
repo: sanitizedRepoName,
ref: `heads/${defaultBranch}`,
});
parentCommitSha = refData.object.sha;
@@ -361,7 +405,7 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
const { data: commitData } = await octokit.git.createCommit({
owner: connection.user.login,
repo: repoName,
repo: sanitizedRepoName,
message: !repoExists ? 'Initial commit from Bolt.diy' : 'Update from Bolt.diy',
tree: treeData.sha,
parents: parentCommitSha ? [parentCommitSha] : [], // Use parent if available
@@ -374,7 +418,7 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
console.log(`Updating reference: heads/${defaultBranch} to ${commitData.sha}`);
await octokit.git.updateRef({
owner: connection.user.login,
repo: repoName,
repo: sanitizedRepoName,
ref: `heads/${defaultBranch}`,
sha: commitData.sha,
force: true, // Use force to ensure the update works
@@ -387,7 +431,7 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
try {
await octokit.git.createRef({
owner: connection.user.login,
repo: repoName,
repo: sanitizedRepoName,
ref: `refs/heads/${defaultBranch}`,
sha: commitData.sha,
});
@@ -413,12 +457,13 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
}
// Save the repository information for this chat
const sanitizedRepoName = sanitizeRepoName(repoName);
localStorage.setItem(
`github-repo-${currentChatId}`,
JSON.stringify({
owner: connection.user.login,
name: repoName,
url: `https://github.com/${connection.user.login}/${repoName}`,
name: sanitizedRepoName,
url: `https://github.com/${connection.user.login}/${sanitizedRepoName}`,
}),
);
@@ -428,14 +473,42 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
console.error('Error pushing to GitHub:', error);
// Attempt to extract more specific error information
let errorMessage = 'Failed to push to GitHub.';
let errorMessage = 'Failed to push to GitHub';
let isRetryable = false;
if (error instanceof Error) {
errorMessage = error.message;
const errorMsg = error.message.toLowerCase();
if (errorMsg.includes('network') || errorMsg.includes('fetch failed') || errorMsg.includes('connection')) {
errorMessage = 'Network error. Please check your internet connection and try again.';
isRetryable = true;
} else if (errorMsg.includes('401') || errorMsg.includes('unauthorized')) {
errorMessage = 'GitHub authentication failed. Please check your access token in Settings > Connections.';
} else if (errorMsg.includes('403') || errorMsg.includes('forbidden')) {
errorMessage =
'Access denied. Your GitHub token may not have sufficient permissions to create/modify repositories.';
} else if (errorMsg.includes('404') || errorMsg.includes('not found')) {
errorMessage = 'Repository or resource not found. Please check the repository name and your permissions.';
} else if (errorMsg.includes('422') || errorMsg.includes('validation failed')) {
if (errorMsg.includes('name already exists')) {
errorMessage =
'A repository with this name already exists in your account. Please choose a different name.';
} else {
errorMessage = 'Repository validation failed. Please check the repository name and settings.';
}
} else if (errorMsg.includes('rate limit') || errorMsg.includes('429')) {
errorMessage = 'GitHub API rate limit exceeded. Please wait a moment and try again.';
isRetryable = true;
} else if (errorMsg.includes('timeout')) {
errorMessage = 'Request timed out. Please check your connection and try again.';
isRetryable = true;
} else {
errorMessage = `GitHub error: ${error.message}`;
}
} else if (typeof error === 'object' && error !== null) {
// Octokit errors
if ('message' in error) {
errorMessage = error.message as string;
errorMessage = `GitHub API error: ${error.message as string}`;
}
// GitHub API errors
@@ -444,7 +517,17 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
}
}
toast.error(`GitHub deployment failed: ${errorMessage}`);
// Show error with retry suggestion if applicable
const finalMessage = isRetryable ? `${errorMessage} Click to retry.` : errorMessage;
toast.error(finalMessage);
// Log detailed error for debugging
console.error('Detailed GitHub deployment error:', {
error,
repoName: sanitizeRepoName(repoName),
user: connection?.user?.login,
isRetryable,
});
} finally {
setIsLoading(false);
}
@@ -488,6 +571,7 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
className="bg-white dark:bg-bolt-elements-background-depth-1 rounded-lg border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark shadow-xl"
aria-describedby="success-dialog-description"
>
<Dialog.Title className="sr-only">Successfully pushed to GitHub</Dialog.Title>
<div className="p-6 space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -624,6 +708,7 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
className="bg-white dark:bg-bolt-elements-background-depth-1 rounded-lg p-6 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark shadow-xl"
aria-describedby="connection-required-description"
>
<Dialog.Title className="sr-only">GitHub Connection Required</Dialog.Title>
<div className="relative text-center space-y-4">
<Dialog.Close asChild>
<button
@@ -763,12 +848,36 @@ export function GitHubDeploymentDialog({ isOpen, onClose, projectName, files }:
id="repoName"
type="text"
value={repoName}
onChange={(e) => setRepoName(e.target.value)}
onChange={(e) => {
const value = e.target.value;
setRepoName(value);
// Show real-time feedback for invalid characters
const sanitized = sanitizeRepoName(value);
if (value && value !== sanitized) {
// Show preview of sanitized name without being too intrusive
e.target.setAttribute('data-sanitized', sanitized);
} else {
e.target.removeAttribute('data-sanitized');
}
}}
placeholder="my-awesome-project"
className="w-full pl-10 px-4 py-2 rounded-lg bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark placeholder-bolt-elements-textTertiary dark:placeholder-bolt-elements-textTertiary-dark focus:outline-none focus:ring-2 focus:ring-purple-500"
required
maxLength={100}
pattern="[a-zA-Z0-9\-_\s]+"
title="Repository name can contain letters, numbers, hyphens, underscores, and spaces"
/>
</div>
{repoName && sanitizeRepoName(repoName) !== repoName && (
<p className="text-xs text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary-dark mt-1">
Will be created as:{' '}
<span className="font-mono text-purple-600 dark:text-purple-400">
{sanitizeRepoName(repoName)}
</span>
</p>
)}
</div>
<div className="space-y-2">

View File

@@ -11,6 +11,7 @@ import { useStore } from '@nanostores/react';
import { GitLabApiService } from '~/lib/services/gitlabApiService';
import { SearchInput, EmptyState, StatusIndicator, Badge } from '~/components/ui';
import { formatSize } from '~/utils/formatSize';
import { GitLabAuthDialog } from '~/components/@settings/tabs/gitlab/components/GitLabAuthDialog';
interface GitLabDeploymentDialogProps {
isOpen: boolean;
@@ -31,6 +32,7 @@ export function GitLabDeploymentDialog({ isOpen, onClose, projectName, files }:
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
const [createdRepoUrl, setCreatedRepoUrl] = useState('');
const [pushedFiles, setPushedFiles] = useState<{ path: string; size: number }[]>([]);
const [showAuthDialog, setShowAuthDialog] = useState(false);
const currentChatId = useStore(chatId);
// Load GitLab connection on mount
@@ -114,12 +116,24 @@ export function GitLabDeploymentDialog({ isOpen, onClose, projectName, files }:
setIsLoading(true);
// Sanitize repository name to match what the API will create
const sanitizedRepoName = repoName
.replace(/[^a-zA-Z0-9-_.]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.toLowerCase();
try {
const gitlabUrl = connection.gitlabUrl || 'https://gitlab.com';
const apiService = new GitLabApiService(connection.token, gitlabUrl);
// Check if project exists
const projectPath = `${connection.user.username}/${repoName}`;
// Warn user if repository name was changed
if (sanitizedRepoName !== repoName && sanitizedRepoName !== repoName.toLowerCase()) {
toast.info(`Repository name sanitized to "${sanitizedRepoName}" to meet GitLab requirements`);
}
// Check if project exists using the sanitized name
const projectPath = `${connection.user.username}/${sanitizedRepoName}`;
const existingProject = await apiService.getProjectByPath(projectPath);
const projectExists = existingProject !== null;
@@ -131,7 +145,7 @@ export function GitLabDeploymentDialog({ isOpen, onClose, projectName, files }:
: '';
const confirmOverwrite = window.confirm(
`Repository "${repoName}" already exists. Do you want to update it? This will add or modify files in the repository.${visibilityChange}`,
`Repository "${sanitizedRepoName}" already exists. Do you want to update it? This will add or modify files in the repository.${visibilityChange}`,
);
if (!confirmOverwrite) {
@@ -154,7 +168,7 @@ export function GitLabDeploymentDialog({ isOpen, onClose, projectName, files }:
// Create new project with files
toast.info('Creating new repository...');
const newProject = await apiService.createProjectWithFiles(repoName, isPrivate, files);
const newProject = await apiService.createProjectWithFiles(sanitizedRepoName, isPrivate, files);
setCreatedRepoUrl(newProject.http_url_to_repo);
toast.success('Repository created successfully!');
}
@@ -173,7 +187,7 @@ export function GitLabDeploymentDialog({ isOpen, onClose, projectName, files }:
`gitlab-repo-${currentChatId}`,
JSON.stringify({
owner: connection.user.username,
name: repoName,
name: sanitizedRepoName,
url: createdRepoUrl,
}),
);
@@ -181,17 +195,18 @@ export function GitLabDeploymentDialog({ isOpen, onClose, projectName, files }:
logStore.logInfo('GitLab deployment completed successfully', {
type: 'system',
message: `Successfully deployed ${fileList.length} files to ${projectExists ? 'existing' : 'new'} GitLab repository: ${projectPath}`,
repoName,
repoName: sanitizedRepoName,
projectPath,
filesCount: fileList.length,
isNewProject: !projectExists,
});
} catch (error) {
console.error('Error pushing to GitLab:', error);
logStore.logError('GitLab deployment failed', {
error,
repoName,
projectPath: `${connection.user.username}/${repoName}`,
repoName: sanitizedRepoName,
projectPath: `${connection.user.username}/${sanitizedRepoName}`,
});
// Provide specific error messages based on error type
@@ -233,6 +248,18 @@ export function GitLabDeploymentDialog({ isOpen, onClose, projectName, files }:
onClose();
};
const handleAuthDialogClose = () => {
setShowAuthDialog(false);
// Refresh user data after auth
const connection = getLocalStorage('gitlab_connection');
if (connection?.user && connection?.token) {
setUser(connection.user);
fetchRecentRepos(connection.token, connection.gitlabUrl || 'https://gitlab.com');
}
};
// Success Dialog
if (showSuccessDialog) {
return (
@@ -425,21 +452,24 @@ export function GitLabDeploymentDialog({ isOpen, onClose, projectName, files }:
>
Close
</motion.button>
<motion.a
href="/settings/connections"
<motion.button
onClick={() => setShowAuthDialog(true)}
className="px-4 py-2 rounded-lg bg-orange-500 text-white text-sm hover:bg-orange-600 inline-flex items-center gap-2"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="i-ph:gear" />
Go to Settings
</motion.a>
<div className="i-ph:gitlab-logo w-4 h-4" />
Connect GitLab Account
</motion.button>
</div>
</div>
</Dialog.Content>
</motion.div>
</div>
</Dialog.Portal>
{/* GitLab Auth Dialog */}
<GitLabAuthDialog isOpen={showAuthDialog} onClose={handleAuthDialogClose} />
</Dialog.Root>
);
}
@@ -499,6 +529,8 @@ export function GitLabDeploymentDialog({ isOpen, onClose, projectName, files }:
src={user.avatar_url}
alt={user.username}
className="w-10 h-10 rounded-full object-cover"
crossOrigin="anonymous"
referrerPolicy="no-referrer"
onError={(e) => {
// Handle CORS/COEP errors by hiding the image and showing fallback
const target = e.target as HTMLImageElement;
@@ -724,6 +756,9 @@ export function GitLabDeploymentDialog({ isOpen, onClose, projectName, files }:
</motion.div>
</div>
</Dialog.Portal>
{/* GitLab Auth Dialog */}
<GitLabAuthDialog isOpen={showAuthDialog} onClose={handleAuthDialogClose} />
</Dialog.Root>
);
}