From 2d1d3ef153f8d36640af69e929f708a440a2ed4c Mon Sep 17 00:00:00 2001 From: Andy <119136210+AndyMik90@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:56:37 +0100 Subject: [PATCH] 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 * 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 --------- Co-authored-by: Claude Sonnet 4.5 --- .../ipc-handlers/ideation/task-converter.ts | 51 ++++++++++----- .../components/ideation/IdeaDetailPanel.tsx | 23 ++++--- .../renderer/components/ideation/Ideation.tsx | 2 + .../components/ideation/hooks/useIdeation.ts | 50 ++++++++++++-- .../src/shared/i18n/locales/en/common.json | 12 ++++ .../src/shared/i18n/locales/fr/common.json | 12 ++++ package-lock.json | 65 ++++++++----------- 7 files changed, 144 insertions(+), 71 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/ideation/task-converter.ts b/apps/frontend/src/main/ipc-handlers/ideation/task-converter.ts index 34b593c8..4141f032 100644 --- a/apps/frontend/src/main/ipc-handlers/ideation/task-converter.ts +++ b/apps/frontend/src/main/ipc-handlers/ideation/task-converter.ts @@ -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); diff --git a/apps/frontend/src/renderer/components/ideation/IdeaDetailPanel.tsx b/apps/frontend/src/renderer/components/ideation/IdeaDetailPanel.tsx index ec4b9475..26340153 100644 --- a/apps/frontend/src/renderer/components/ideation/IdeaDetailPanel.tsx +++ b/apps/frontend/src/renderer/components/ideation/IdeaDetailPanel.tsx @@ -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
{/* Description */}
-

Description

+

{t('common:ideation.description')}

{idea.description}

@@ -74,7 +75,7 @@ export function IdeaDetailPanel({ idea, onClose, onConvert, onGoToTask, onDismis

- Rationale + {t('common:ideation.rationale')}

{idea.rationale}

@@ -91,9 +92,13 @@ export function IdeaDetailPanel({ idea, onClose, onConvert, onGoToTask, onDismis {/* Actions */} {!isDismissed && !isConverted && (
-
)} @@ -112,7 +117,7 @@ export function IdeaDetailPanel({ idea, onClose, onConvert, onGoToTask, onDismis
)} diff --git a/apps/frontend/src/renderer/components/ideation/Ideation.tsx b/apps/frontend/src/renderer/components/ideation/Ideation.tsx index d372aa70..ce5feaa0 100644 --- a/apps/frontend/src/renderer/components/ideation/Ideation.tsx +++ b/apps/frontend/src/renderer/components/ideation/Ideation.tsx @@ -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)} /> )} diff --git a/apps/frontend/src/renderer/components/ideation/hooks/useIdeation.ts b/apps/frontend/src/renderer/components/ideation/hooks/useIdeation.ts index 71359c98..0a6c7b22 100644 --- a/apps/frontend/src/renderer/components/ideation/hooks/useIdeation.ts +++ b/apps/frontend/src/renderer/components/ideation/hooks/useIdeation.ts @@ -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([]); + const [convertingIdeas, setConvertingIdeas] = useState>(new Set()); + // Ref for synchronous tracking - prevents race condition from stale React state closure + const convertingIdeaRef = useRef>(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, diff --git a/apps/frontend/src/shared/i18n/locales/en/common.json b/apps/frontend/src/shared/i18n/locales/en/common.json index a66c0999..aa11f3e2 100644 --- a/apps/frontend/src/shared/i18n/locales/en/common.json +++ b/apps/frontend/src/shared/i18n/locales/en/common.json @@ -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" } } diff --git a/apps/frontend/src/shared/i18n/locales/fr/common.json b/apps/frontend/src/shared/i18n/locales/fr/common.json index b2843787..3861d444 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/common.json +++ b/apps/frontend/src/shared/i18n/locales/fr/common.json @@ -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" } } diff --git a/package-lock.json b/package-lock.json index 842c9c6a..2f271430 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }