Fix Duplicate Kanban Task Creation on Rapid Button Clicks (#1021)
* auto-claude: subtask-1-1 - Add convertingIdeas state and guard logic to useIdeation hook * auto-claude: subtask-1-2 - Update IdeaDetailPanel to accept isConverting prop * auto-claude: subtask-2-1 - Add idempotency check for linked_task_id in task-c * auto-claude: subtask-3-1 - Manual testing: Verify rapid clicking creates only one task - Fixed missing convertingIdeas prop connection in Ideation.tsx - Added convertingIdeas to destructured hook values - Added isConverting prop to IdeaDetailPanel component - Created detailed manual-test-report.md with code review and E2E testing instructions - All code implementation verified via TypeScript checks (no errors) - Multi-layer protection confirmed: UI disabled, guard check, backend idempotency - Manual E2E testing required for final verification Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: address PR review findings for duplicate task prevention - Fix TOCTOU race condition by moving idempotency check inside lock - Fix React state closure by using ref for synchronous tracking - Add i18n translations for ideation UI (EN + FR) - Add error handling with toast notifications for conversion failures Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -194,29 +194,46 @@ export async function convertIdeaToTask(
|
||||
AUTO_BUILD_PATHS.IDEATION_FILE
|
||||
);
|
||||
|
||||
const ideation = readIdeationFile(ideationPath);
|
||||
if (!ideation) {
|
||||
// Quick check that ideation file exists (actual read happens inside lock)
|
||||
if (!existsSync(ideationPath)) {
|
||||
return { success: false, error: 'Ideation not found' };
|
||||
}
|
||||
|
||||
// Get specs directory path
|
||||
const specsBaseDir = getSpecsDir(project.autoBuildPath);
|
||||
const specsDir = path.join(project.path, specsBaseDir);
|
||||
|
||||
// Ensure specs directory exists
|
||||
if (!existsSync(specsDir)) {
|
||||
mkdirSync(specsDir, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
// Find the idea
|
||||
const idea = ideation.ideas?.find((i) => i.id === ideaId);
|
||||
if (!idea) {
|
||||
return { success: false, error: 'Idea not found' };
|
||||
}
|
||||
|
||||
// Get specs directory path
|
||||
const specsBaseDir = getSpecsDir(project.autoBuildPath);
|
||||
const specsDir = path.join(project.path, specsBaseDir);
|
||||
|
||||
// Ensure specs directory exists
|
||||
if (!existsSync(specsDir)) {
|
||||
mkdirSync(specsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Use coordinated spec numbering with lock to prevent collisions
|
||||
// CRITICAL: All state checks must happen INSIDE the lock to prevent TOCTOU race conditions
|
||||
return await withSpecNumberLock(project.path, async (lock) => {
|
||||
// Re-read ideation file INSIDE the lock to get fresh state
|
||||
const ideation = readIdeationFile(ideationPath);
|
||||
if (!ideation) {
|
||||
return { success: false, error: 'Ideation not found' };
|
||||
}
|
||||
|
||||
// Find the idea (inside lock for fresh state)
|
||||
const idea = ideation.ideas?.find((i) => i.id === ideaId);
|
||||
if (!idea) {
|
||||
return { success: false, error: 'Idea not found' };
|
||||
}
|
||||
|
||||
// Idempotency check INSIDE lock - prevents TOCTOU race condition
|
||||
// Two concurrent requests can both pass an outside check, but only one
|
||||
// can hold the lock at a time, so this check is authoritative
|
||||
if (idea.linked_task_id) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Idea has already been converted to task: ${idea.linked_task_id}`
|
||||
};
|
||||
}
|
||||
|
||||
// Get next spec number from global scan (main + all worktrees)
|
||||
const nextNum = lock.getNextSpecNumber(project.autoBuildPath);
|
||||
const slugifiedTitle = slugifyTitle(idea.title);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChevronRight, ExternalLink, Lightbulb, Play, X } from 'lucide-react';
|
||||
import { ChevronRight, ExternalLink, Lightbulb, Loader2, Play, X } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import { Badge } from '../ui/badge';
|
||||
import {
|
||||
@@ -30,9 +30,10 @@ interface IdeaDetailPanelProps {
|
||||
onConvert: (idea: Idea) => void;
|
||||
onGoToTask?: (taskId: string) => void;
|
||||
onDismiss: (idea: Idea) => void;
|
||||
isConverting?: boolean;
|
||||
}
|
||||
|
||||
export function IdeaDetailPanel({ idea, onClose, onConvert, onGoToTask, onDismiss }: IdeaDetailPanelProps) {
|
||||
export function IdeaDetailPanel({ idea, onClose, onConvert, onGoToTask, onDismiss, isConverting }: IdeaDetailPanelProps) {
|
||||
const { t } = useTranslation('common');
|
||||
const isDismissed = idea.status === 'dismissed';
|
||||
const isConverted = idea.status === 'converted';
|
||||
@@ -66,7 +67,7 @@ export function IdeaDetailPanel({ idea, onClose, onConvert, onGoToTask, onDismis
|
||||
<div className="flex-1 overflow-auto p-4 space-y-6">
|
||||
{/* Description */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">Description</h3>
|
||||
<h3 className="text-sm font-medium mb-2">{t('common:ideation.description')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{idea.description}</p>
|
||||
</div>
|
||||
|
||||
@@ -74,7 +75,7 @@ export function IdeaDetailPanel({ idea, onClose, onConvert, onGoToTask, onDismis
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
|
||||
<Lightbulb className="h-4 w-4" />
|
||||
Rationale
|
||||
{t('common:ideation.rationale')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">{idea.rationale}</p>
|
||||
</div>
|
||||
@@ -91,9 +92,13 @@ export function IdeaDetailPanel({ idea, onClose, onConvert, onGoToTask, onDismis
|
||||
{/* Actions */}
|
||||
{!isDismissed && !isConverted && (
|
||||
<div className="shrink-0 p-4 border-t border-border space-y-2">
|
||||
<Button className="w-full" onClick={() => onConvert(idea)}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Convert to Auto-Build Task
|
||||
<Button className="w-full" onClick={() => onConvert(idea)} disabled={isConverting}>
|
||||
{isConverting ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{isConverting ? t('common:ideation.converting') : t('common:ideation.convertToTask')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -104,7 +109,7 @@ export function IdeaDetailPanel({ idea, onClose, onConvert, onGoToTask, onDismis
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Dismiss Idea
|
||||
{t('common:ideation.dismissIdea')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -112,7 +117,7 @@ export function IdeaDetailPanel({ idea, onClose, onConvert, onGoToTask, onDismis
|
||||
<div className="shrink-0 p-4 border-t border-border">
|
||||
<Button className="w-full" onClick={() => onGoToTask(idea.taskId!)}>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
Go to Task
|
||||
{t('common:ideation.goToTask')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -41,6 +41,7 @@ export function Ideation({ projectId, onGoToTask }: IdeationProps) {
|
||||
summary,
|
||||
activeIdeas,
|
||||
selectedIds,
|
||||
convertingIdeas,
|
||||
setSelectedIdea,
|
||||
setActiveTab,
|
||||
setShowConfigDialog,
|
||||
@@ -217,6 +218,7 @@ export function Ideation({ projectId, onGoToTask }: IdeationProps) {
|
||||
onConvert={handleConvertToTask}
|
||||
onGoToTask={handleGoToTask}
|
||||
onDismiss={handleDismiss}
|
||||
isConverting={convertingIdeas.has(selectedIdea.id)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from '../../../hooks/use-toast';
|
||||
import {
|
||||
useIdeationStore,
|
||||
loadIdeation,
|
||||
@@ -27,6 +29,7 @@ interface UseIdeationOptions {
|
||||
|
||||
export function useIdeation(projectId: string, options: UseIdeationOptions = {}) {
|
||||
const { onGoToTask, showArchived: externalShowArchived } = options;
|
||||
const { t } = useTranslation('common');
|
||||
const session = useIdeationStore((state) => state.session);
|
||||
const generationStatus = useIdeationStore((state) => state.generationStatus);
|
||||
const isGenerating = useIdeationStore((state) => state.isGenerating);
|
||||
@@ -48,6 +51,9 @@ export function useIdeation(projectId: string, options: UseIdeationOptions = {})
|
||||
const [pendingAction, setPendingAction] = useState<'generate' | 'refresh' | 'append' | null>(null);
|
||||
const [showAddMoreDialog, setShowAddMoreDialog] = useState(false);
|
||||
const [typesToAdd, setTypesToAdd] = useState<IdeationType[]>([]);
|
||||
const [convertingIdeas, setConvertingIdeas] = useState<Set<string>>(new Set());
|
||||
// Ref for synchronous tracking - prevents race condition from stale React state closure
|
||||
const convertingIdeaRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const { hasToken, isLoading: isCheckingToken, checkAuth } = useIdeationAuth();
|
||||
|
||||
@@ -130,11 +136,42 @@ export function useIdeation(projectId: string, options: UseIdeationOptions = {})
|
||||
};
|
||||
|
||||
const handleConvertToTask = async (idea: Idea) => {
|
||||
const result = await window.electronAPI.convertIdeaToTask(projectId, idea.id);
|
||||
if (result.success && result.data) {
|
||||
// Store the taskId on the idea so we can navigate to it later
|
||||
useIdeationStore.getState().setIdeaTaskId(idea.id, result.data.id);
|
||||
loadTasks(projectId);
|
||||
// Guard: use ref for synchronous check to prevent race condition from stale state closure
|
||||
// React state is captured at render time, so rapid clicks would both see empty set
|
||||
if (convertingIdeaRef.current.has(idea.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as converting - update ref synchronously first, then state for UI
|
||||
convertingIdeaRef.current.add(idea.id);
|
||||
setConvertingIdeas(new Set(convertingIdeaRef.current));
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.convertIdeaToTask(projectId, idea.id);
|
||||
if (result.success && result.data) {
|
||||
// Store the taskId on the idea so we can navigate to it later
|
||||
useIdeationStore.getState().setIdeaTaskId(idea.id, result.data.id);
|
||||
loadTasks(projectId);
|
||||
} else {
|
||||
// Show error toast when conversion fails (e.g., already converted, idea not found)
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t('ideation.conversionFailed'),
|
||||
description: result.error || t('ideation.conversionFailedDescription')
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle unexpected errors (network issues, etc.)
|
||||
console.error('Failed to convert idea to task:', error);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t('ideation.conversionError'),
|
||||
description: t('ideation.conversionErrorDescription')
|
||||
});
|
||||
} finally {
|
||||
// Always clear converting state - update ref first, then state
|
||||
convertingIdeaRef.current.delete(idea.id);
|
||||
setConvertingIdeas(new Set(convertingIdeaRef.current));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -228,6 +265,7 @@ export function useIdeation(projectId: string, options: UseIdeationOptions = {})
|
||||
activeIdeas,
|
||||
archivedIdeas,
|
||||
selectedIds,
|
||||
convertingIdeas,
|
||||
|
||||
// Actions
|
||||
setSelectedIdea,
|
||||
|
||||
@@ -352,5 +352,17 @@
|
||||
"done": "Done",
|
||||
"failedLabel": "Failed",
|
||||
"starting": "Starting..."
|
||||
},
|
||||
"ideation": {
|
||||
"converting": "Converting...",
|
||||
"convertToTask": "Convert to Auto-Build Task",
|
||||
"dismissIdea": "Dismiss Idea",
|
||||
"description": "Description",
|
||||
"rationale": "Rationale",
|
||||
"goToTask": "Go to Task",
|
||||
"conversionFailed": "Conversion failed",
|
||||
"conversionFailedDescription": "Failed to convert idea to task",
|
||||
"conversionError": "Conversion error",
|
||||
"conversionErrorDescription": "An error occurred while converting the idea"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,5 +352,17 @@
|
||||
"done": "Terminé",
|
||||
"failedLabel": "Échoué",
|
||||
"starting": "Démarrage..."
|
||||
},
|
||||
"ideation": {
|
||||
"converting": "Conversion...",
|
||||
"convertToTask": "Convertir en tâche Auto-Build",
|
||||
"dismissIdea": "Ignorer l'idée",
|
||||
"description": "Description",
|
||||
"rationale": "Justification",
|
||||
"goToTask": "Aller à la tâche",
|
||||
"conversionFailed": "Échec de la conversion",
|
||||
"conversionFailedDescription": "Impossible de convertir l'idée en tâche",
|
||||
"conversionError": "Erreur de conversion",
|
||||
"conversionErrorDescription": "Une erreur s'est produite lors de la conversion de l'idée"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+26
-39
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "auto-claude",
|
||||
"version": "2.7.2",
|
||||
"version": "2.7.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "auto-claude",
|
||||
"version": "2.7.2",
|
||||
"version": "2.7.4",
|
||||
"license": "AGPL-3.0",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"apps/frontend": {
|
||||
"name": "auto-claude-ui",
|
||||
"version": "2.7.2",
|
||||
"version": "2.7.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -360,7 +360,6 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -745,7 +744,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -789,7 +787,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -829,7 +826,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -1695,6 +1691,7 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cross-dirname": "^0.1.0",
|
||||
"debug": "^4.3.4",
|
||||
@@ -1716,6 +1713,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -1732,6 +1730,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
@@ -1746,6 +1745,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
@@ -2807,7 +2807,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -2829,7 +2828,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz",
|
||||
"integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
@@ -2842,7 +2840,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
|
||||
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
@@ -2858,7 +2855,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz",
|
||||
"integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.208.0",
|
||||
"import-in-the-middle": "^2.0.0",
|
||||
@@ -3246,7 +3242,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -3263,7 +3258,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
|
||||
"integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0",
|
||||
@@ -3281,7 +3275,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz",
|
||||
"integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
@@ -5519,7 +5512,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -5750,7 +5744,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -5761,7 +5754,6 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -5869,7 +5861,6 @@
|
||||
"integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.51.0",
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
@@ -6284,7 +6275,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6354,7 +6344,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -6975,7 +6964,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -7789,7 +7777,8 @@
|
||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.1.0",
|
||||
@@ -8175,7 +8164,6 @@
|
||||
"integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "26.0.12",
|
||||
"builder-util": "26.0.11",
|
||||
@@ -8271,7 +8259,8 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
@@ -8346,7 +8335,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^22.7.7",
|
||||
@@ -8595,6 +8583,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"debug": "^4.1.1",
|
||||
@@ -8615,6 +8604,7 @@
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
@@ -8989,7 +8979,6 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -10215,7 +10204,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
@@ -11018,7 +11006,6 @@
|
||||
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.28",
|
||||
"@asamuzakjp/dom-selector": "^6.7.6",
|
||||
@@ -11755,6 +11742,7 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -13824,7 +13812,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -13900,6 +13887,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^9.4.0"
|
||||
},
|
||||
@@ -13917,6 +13905,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
@@ -13937,6 +13926,7 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -13952,6 +13942,7 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -14079,7 +14070,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14089,7 +14079,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -14129,7 +14118,8 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "10.1.0",
|
||||
@@ -14564,6 +14554,7 @@
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
@@ -15454,8 +15445,7 @@
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
@@ -15565,6 +15555,7 @@
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
@@ -15628,6 +15619,7 @@
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
@@ -15719,7 +15711,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15982,7 +15973,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16332,7 +16322,6 @@
|
||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -16925,7 +16914,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -17466,7 +17454,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.2.tgz",
|
||||
"integrity": "sha512-b8L8yn4rIVfiXyHAmnr52/ZEpDumlT0bmxiq3Ws1ybrinhflGpt12Hvv54kYnEsGPRs6o/Ka3/ppA2OWY21IVg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user