diff --git a/AGENTS.md b/AGENTS.md index d11e6bd7..d5d53b84 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,7 @@ ## Project Structure & Module Organization - `crates/`: Rust workspace crates — `server` (API + bins), `db` (SQLx models/migrations), `executors`, `services`, `utils`, `deployment`, `local-deployment`. - `frontend/`: React + TypeScript app (Vite, Tailwind). Source in `frontend/src`. +- `frontend/src/components/dialogs`: Dialog components for the frontend. - `shared/`: Generated TypeScript types (`shared/types.ts`). Do not edit directly. - `assets/`, `dev_assets_seed/`, `dev_assets/`: Packaged and local dev assets. - `npx-cli/`: Files published to the npm CLI package. diff --git a/STYLE_OVERRIDE.md b/STYLE_OVERRIDE.md deleted file mode 100644 index b96af621..00000000 --- a/STYLE_OVERRIDE.md +++ /dev/null @@ -1,57 +0,0 @@ -# Style Override via postMessage - -Simple API for overriding styles when embedding the frontend in an iframe. - -## Usage - -```javascript -// Switch theme -iframe.contentWindow.postMessage({ - type: 'VIBE_STYLE', - theme: 'purple' // 'system', 'light', 'dark', 'purple', 'green', 'blue', 'orange', 'red' -}, 'https://your-app-domain.com'); - -// Override CSS variables (--vibe-* prefix only) -iframe.contentWindow.postMessage({ - type: 'VIBE_STYLE', - css: { - '--vibe-primary': '220 14% 96%', - '--vibe-background': '0 0% 100%' - } -}, 'https://your-app-domain.com'); - -// Both together -iframe.contentWindow.postMessage({ - type: 'VIBE_STYLE', - theme: 'dark', - css: { - '--vibe-accent': '210 100% 50%' - } -}, 'https://your-app-domain.com'); -``` - -## Security - -- Origin validation via `VITE_PARENT_ORIGIN` environment variable -- Only `--vibe-*` prefixed CSS variables can be overridden -- Browser validates CSS values automatically - -## Example - -```html - - -``` diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0e1e3fd4..0545f0e5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { BrowserRouter, Route, @@ -16,171 +16,103 @@ import { AgentSettings, McpSettings, } from '@/pages/settings/'; -import { DisclaimerDialog } from '@/components/DisclaimerDialog'; -import { OnboardingDialog } from '@/components/OnboardingDialog'; -import { PrivacyOptInDialog } from '@/components/PrivacyOptInDialog'; -import { ConfigProvider, useConfig } from '@/components/config-provider'; +import { + UserSystemProvider, + useUserSystem, +} from '@/components/config-provider'; import { ThemeProvider } from '@/components/theme-provider'; import { SearchProvider } from '@/contexts/search-context'; -import { - EditorDialogProvider, - useEditorDialog, -} from '@/contexts/editor-dialog-context'; -import { CreatePRDialogProvider } from '@/contexts/create-pr-dialog-context'; -import { EditorSelectionDialog } from '@/components/tasks/EditorSelectionDialog'; -import CreatePRDialog from '@/components/tasks/Toolbar/CreatePRDialog'; -import { TaskDialogProvider } from '@/contexts/task-dialog-context'; -import { TaskFormDialogContainer } from '@/components/tasks/TaskFormDialogContainer'; + import { ProjectProvider } from '@/contexts/project-context'; -import type { EditorType } from 'shared/types'; import { ThemeMode } from 'shared/types'; -import type { ExecutorProfileId } from 'shared/types'; -import { configApi } from '@/lib/api'; import * as Sentry from '@sentry/react'; import { Loader } from '@/components/ui/loader'; -import { GitHubLoginDialog } from '@/components/GitHubLoginDialog'; -import { ReleaseNotesDialog } from '@/components/ReleaseNotesDialog'; + import { AppWithStyleOverride } from '@/utils/style-override'; import { WebviewContextMenu } from '@/vscode/ContextMenu'; import { DevBanner } from '@/components/DevBanner'; +import NiceModal from '@ebay/nice-modal-react'; +import { OnboardingResult } from './components/dialogs/global/OnboardingDialog'; const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); function AppContent() { - const { config, updateConfig, loading } = useConfig(); + const { config, updateAndSaveConfig, loading } = useUserSystem(); const location = useLocation(); - const { - isOpen: editorDialogOpen, - selectedAttempt, - closeEditorDialog, - } = useEditorDialog(); - const [showDisclaimer, setShowDisclaimer] = useState(false); - const [showOnboarding, setShowOnboarding] = useState(false); - const [showPrivacyOptIn, setShowPrivacyOptIn] = useState(false); - const [showGitHubLogin, setShowGitHubLogin] = useState(false); - const [showReleaseNotes, setShowReleaseNotes] = useState(false); + const showNavbar = !location.pathname.endsWith('/full'); useEffect(() => { - if (config) { - setShowDisclaimer(!config.disclaimer_acknowledged); - if (config.disclaimer_acknowledged) { - setShowOnboarding(!config.onboarding_acknowledged); - if (config.onboarding_acknowledged) { - if (!config.github_login_acknowledged) { - setShowGitHubLogin(true); - } else if (!config.telemetry_acknowledged) { - setShowPrivacyOptIn(true); - } else if (config.show_release_notes) { - setShowReleaseNotes(true); - } - } - } - } - }, [config]); - - const handleDisclaimerAccept = async () => { - if (!config) return; - - updateConfig({ disclaimer_acknowledged: true }); - - try { - await configApi.saveConfig({ ...config, disclaimer_acknowledged: true }); - setShowDisclaimer(false); - setShowOnboarding(!config.onboarding_acknowledged); - } catch (err) { - console.error('Error saving config:', err); - } - }; - - const handleOnboardingComplete = async (onboardingConfig: { - profile: ExecutorProfileId; - editor: { editor_type: EditorType; custom_command: string | null }; - }) => { - if (!config) return; - - const updatedConfig = { - ...config, - onboarding_acknowledged: true, - executor_profile: onboardingConfig.profile, - editor: onboardingConfig.editor, - }; - - updateConfig(updatedConfig); - - try { - await configApi.saveConfig(updatedConfig); - setShowOnboarding(false); - } catch (err) { - console.error('Error saving config:', err); - } - }; - - const handlePrivacyOptInComplete = async (telemetryEnabled: boolean) => { - if (!config) return; - - const updatedConfig = { - ...config, - telemetry_acknowledged: true, - analytics_enabled: telemetryEnabled, - }; - - updateConfig(updatedConfig); - - try { - await configApi.saveConfig(updatedConfig); - setShowPrivacyOptIn(false); - if (updatedConfig.show_release_notes) { - setShowReleaseNotes(true); - } - } catch (err) { - console.error('Error saving config:', err); - } - }; - - const handleGitHubLoginComplete = async () => { - try { - // Refresh the config to get the latest GitHub authentication state - const latestUserSystem = await configApi.getConfig(); - updateConfig(latestUserSystem.config); - setShowGitHubLogin(false); - - // If user skipped (no GitHub token), we need to manually set the acknowledgment - + const handleOnboardingComplete = async ( + onboardingConfig: OnboardingResult + ) => { const updatedConfig = { - ...latestUserSystem.config, - github_login_acknowledged: true, + ...config, + onboarding_acknowledged: true, + executor_profile: onboardingConfig.profile, + editor: onboardingConfig.editor, }; - updateConfig(updatedConfig); - await configApi.saveConfig(updatedConfig); - } catch (err) { - console.error('Error refreshing config:', err); - } finally { - if (!config?.telemetry_acknowledged) { - setShowPrivacyOptIn(true); - } else if (config?.show_release_notes) { - setShowReleaseNotes(true); - } - } - }; - const handleReleaseNotesClose = async () => { - if (!config) return; - - const updatedConfig = { - ...config, - show_release_notes: false, + updateAndSaveConfig(updatedConfig); }; - updateConfig(updatedConfig); + const handleDisclaimerAccept = async () => { + await updateAndSaveConfig({ disclaimer_acknowledged: true }); + }; - try { - await configApi.saveConfig(updatedConfig); - setShowReleaseNotes(false); - } catch (err) { - console.error('Error saving config:', err); - } - }; + const handleGitHubLoginComplete = async () => { + await updateAndSaveConfig({ github_login_acknowledged: true }); + }; + + const handleTelemetryOptIn = async (analyticsEnabled: boolean) => { + await updateAndSaveConfig({ + telemetry_acknowledged: true, + analytics_enabled: analyticsEnabled, + }); + }; + + const handleReleaseNotesClose = async () => { + await updateAndSaveConfig({ show_release_notes: false }); + }; + + const checkOnboardingSteps = async () => { + if (!config) return; + + if (!config.disclaimer_acknowledged) { + await NiceModal.show('disclaimer'); + await handleDisclaimerAccept(); + await NiceModal.hide('disclaimer'); + } + + if (!config.onboarding_acknowledged) { + const onboardingResult: OnboardingResult = + await NiceModal.show('onboarding'); + await handleOnboardingComplete(onboardingResult); + await NiceModal.hide('onboarding'); + } + + if (!config.github_login_acknowledged) { + await NiceModal.show('github-login'); + await handleGitHubLoginComplete(); + await NiceModal.hide('github-login'); + } + + if (!config.telemetry_acknowledged) { + const analyticsEnabled: boolean = + await NiceModal.show('privacy-opt-in'); + await handleTelemetryOptIn(analyticsEnabled); + await NiceModal.hide('privacy-opt-in'); + } + + if (config.show_release_notes) { + await NiceModal.show('release-notes'); + await handleReleaseNotesClose(); + await NiceModal.hide('release-notes'); + } + }; + + checkOnboardingSteps(); + }, [config]); if (loading) { return ( @@ -197,33 +129,7 @@ function AppContent() {
{/* Custom context menu and VS Code-friendly interactions when embedded in iframe */} - - - - - - - - + {showNavbar && } {showNavbar && }
@@ -270,17 +176,13 @@ function AppContent() { function App() { return ( - + - - - - - - - + + + - + ); } diff --git a/frontend/src/components/DiffCard.tsx b/frontend/src/components/DiffCard.tsx index 4ec9e0c7..a4fe20b7 100644 --- a/frontend/src/components/DiffCard.tsx +++ b/frontend/src/components/DiffCard.tsx @@ -2,7 +2,7 @@ import { Diff } from 'shared/types'; import { DiffModeEnum, DiffView } from '@git-diff-view/react'; import { generateDiffFile } from '@git-diff-view/file'; import { useMemo } from 'react'; -import { useConfig } from '@/components/config-provider'; +import { useUserSystem } from '@/components/config-provider'; import { getHighLightLanguageFromPath } from '@/utils/extToLanguage'; import { getActualTheme } from '@/utils/theme'; import { Button } from '@/components/ui/button'; @@ -46,7 +46,7 @@ export default function DiffCard({ onToggle, selectedAttempt, }: Props) { - const { config } = useConfig(); + const { config } = useUserSystem(); const theme = getActualTheme(config?.theme); const oldName = diff.oldPath || undefined; diff --git a/frontend/src/components/NormalizedConversation/EditDiffRenderer.tsx b/frontend/src/components/NormalizedConversation/EditDiffRenderer.tsx index 17a0b813..def0aa2e 100644 --- a/frontend/src/components/NormalizedConversation/EditDiffRenderer.tsx +++ b/frontend/src/components/NormalizedConversation/EditDiffRenderer.tsx @@ -6,7 +6,7 @@ import { parseInstance, } from '@git-diff-view/react'; import { SquarePen } from 'lucide-react'; -import { useConfig } from '@/components/config-provider'; +import { useUserSystem } from '@/components/config-provider'; import { getHighLightLanguageFromPath } from '@/utils/extToLanguage'; import { getActualTheme } from '@/utils/theme'; import '@/styles/diff-style-overrides.css'; @@ -63,7 +63,7 @@ function EditDiffRenderer({ hasLineNumbers, expansionKey, }: Props) { - const { config } = useConfig(); + const { config } = useUserSystem(); const [expanded, setExpanded] = useExpandable(expansionKey, false); const theme = getActualTheme(config?.theme); diff --git a/frontend/src/components/NormalizedConversation/FileChangeRenderer.tsx b/frontend/src/components/NormalizedConversation/FileChangeRenderer.tsx index 2ac96a1d..23d3e387 100644 --- a/frontend/src/components/NormalizedConversation/FileChangeRenderer.tsx +++ b/frontend/src/components/NormalizedConversation/FileChangeRenderer.tsx @@ -1,5 +1,5 @@ import { type FileChange } from 'shared/types'; -import { useConfig } from '@/components/config-provider'; +import { useUserSystem } from '@/components/config-provider'; import { Trash2, FilePlus2, ArrowRight } from 'lucide-react'; import { getHighLightLanguageFromPath } from '@/utils/extToLanguage'; import { getActualTheme } from '@/utils/theme'; @@ -36,7 +36,7 @@ function isEdit( } const FileChangeRenderer = ({ path, change, expansionKey }: Props) => { - const { config } = useConfig(); + const { config } = useUserSystem(); const [expanded, setExpanded] = useExpandable(expansionKey, false); const theme = getActualTheme(config?.theme); diff --git a/frontend/src/components/ProvidePatDialog.tsx b/frontend/src/components/ProvidePatDialog.tsx deleted file mode 100644 index 58775cdb..00000000 --- a/frontend/src/components/ProvidePatDialog.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useState } from 'react'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from './ui/dialog'; -import { Input } from './ui/input'; -import { Button } from './ui/button'; -import { useConfig } from './config-provider'; -import { Alert, AlertDescription } from './ui/alert'; - -export function ProvidePatDialog({ - open, - onOpenChange, - errorMessage, -}: { - open: boolean; - onOpenChange: (open: boolean) => void; - errorMessage?: string; -}) { - const { config, updateAndSaveConfig } = useConfig(); - const [pat, setPat] = useState(''); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); - - const handleSave = async () => { - if (!config) return; - setSaving(true); - setError(null); - try { - await updateAndSaveConfig({ - github: { - ...config.github, - pat, - }, - }); - onOpenChange(false); - } catch (err) { - setError('Failed to save Personal Access Token'); - } finally { - setSaving(false); - } - }; - - return ( - - - - Provide GitHub Personal Access Token - -
-

- {errorMessage || - 'Your GitHub OAuth token does not have sufficient permissions to open a PR in this repository.'} -
-
- Please provide a Personal Access Token with repo permissions. -

- setPat(e.target.value)} - autoFocus - /> -

- - Create a token here - -

- {error && ( - - {error} - - )} -
- - - - -
-
- ); -} diff --git a/frontend/src/components/TaskTemplateManager.tsx b/frontend/src/components/TaskTemplateManager.tsx index 70b90ea7..5a68a983 100644 --- a/frontend/src/components/TaskTemplateManager.tsx +++ b/frontend/src/components/TaskTemplateManager.tsx @@ -1,22 +1,9 @@ import { useState, useEffect, useCallback } from 'react'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from '@/components/ui/dialog'; import { Plus, Edit2, Trash2, Loader2 } from 'lucide-react'; import { templatesApi } from '@/lib/api'; -import type { - TaskTemplate, - CreateTaskTemplate, - UpdateTaskTemplate, -} from 'shared/types'; +import { showTaskTemplateEdit } from '@/lib/modals'; +import type { TaskTemplate } from 'shared/types'; interface TaskTemplateManagerProps { projectId?: string; @@ -29,17 +16,6 @@ export function TaskTemplateManager({ }: TaskTemplateManagerProps) { const [templates, setTemplates] = useState([]); const [loading, setLoading] = useState(true); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [editingTemplate, setEditingTemplate] = useState( - null - ); - const [formData, setFormData] = useState({ - template_name: '', - title: '', - description: '', - }); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); const fetchTemplates = useCallback(async () => { setLoading(true); @@ -69,96 +45,24 @@ export function TaskTemplateManager({ fetchTemplates(); }, [fetchTemplates]); - const handleOpenDialog = useCallback((template?: TaskTemplate) => { - if (template) { - setEditingTemplate(template); - setFormData({ - template_name: template.template_name, - title: template.title, - description: template.description || '', - }); - } else { - setEditingTemplate(null); - setFormData({ - template_name: '', - title: '', - description: '', - }); - } - setError(null); - setIsDialogOpen(true); - }, []); + const handleOpenDialog = useCallback( + async (template?: TaskTemplate) => { + try { + const result = await showTaskTemplateEdit({ + template: template || null, + projectId, + isGlobal, + }); - const handleCloseDialog = useCallback(() => { - setIsDialogOpen(false); - setEditingTemplate(null); - setFormData({ - template_name: '', - title: '', - description: '', - }); - setError(null); - }, []); - - const handleSave = useCallback(async () => { - if (!formData.template_name.trim() || !formData.title.trim()) { - setError('Template name and title are required'); - return; - } - - setSaving(true); - setError(null); - - try { - if (editingTemplate) { - const updateData: UpdateTaskTemplate = { - template_name: formData.template_name, - title: formData.title, - description: formData.description || null, - }; - await templatesApi.update(editingTemplate.id, updateData); - } else { - const createData: CreateTaskTemplate = { - project_id: isGlobal ? null : projectId || null, - template_name: formData.template_name, - title: formData.title, - description: formData.description || null, - }; - await templatesApi.create(createData); - } - await fetchTemplates(); - handleCloseDialog(); - } catch (err: any) { - setError(err.message || 'Failed to save template'); - } finally { - setSaving(false); - } - }, [ - formData, - editingTemplate, - isGlobal, - projectId, - fetchTemplates, - handleCloseDialog, - ]); - - // Handle keyboard shortcuts - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - // Command/Ctrl + Enter to save template - if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { - if (isDialogOpen && !saving) { - event.preventDefault(); - handleSave(); + if (result === 'saved') { + await fetchTemplates(); } + } catch (error) { + // User cancelled - do nothing } - }; - - if (isDialogOpen) { - document.addEventListener('keydown', handleKeyDown, true); // Use capture phase for priority - return () => document.removeEventListener('keydown', handleKeyDown, true); - } - }, [isDialogOpen, saving, handleSave]); + }, + [projectId, isGlobal, fetchTemplates] + ); const handleDelete = useCallback( async (template: TaskTemplate) => { @@ -271,66 +175,6 @@ export function TaskTemplateManager({
)} - - - - - - {editingTemplate ? 'Edit Template' : 'Create Template'} - - -
-
- - - setFormData({ ...formData, template_name: e.target.value }) - } - placeholder="e.g., Bug Fix, Feature Request" - /> -
-
- - - setFormData({ ...formData, title: e.target.value }) - } - placeholder="e.g., Fix bug in..." - /> -
-
- -