feat: migrate to React 19
Upgrade React 18.3.1 → 19.2.4 with full type compatibility. Changes: - react, react-dom → ^19.0.0 - @types/react, @types/react-dom → ^19.0.0 - lucide-react → ^0.577.0 (React 19 type fixes) - @tiptap/* → ^3.20.4 (React 19 support) - useRef calls now require explicit initial value (undefined) - RefObject types updated for React 19 (includes null) - MutableRefObject → RefObject (deprecated in 19) - act() import moved from react-dom/test-utils to react - Scoped JSX namespace imports added where needed
This commit is contained in:
parent
e0d4782c80
commit
47dac2e8b5
28 changed files with 747 additions and 748 deletions
20
package.json
20
package.json
|
|
@ -112,11 +112,11 @@
|
|||
"@sentry/electron": "^7.10.0",
|
||||
"@sentry/react": "^10.45.0",
|
||||
"@tanstack/react-virtual": "^3.10.8",
|
||||
"@tiptap/extension-placeholder": "^3.20.1",
|
||||
"@tiptap/markdown": "^3.20.1",
|
||||
"@tiptap/pm": "^3.20.1",
|
||||
"@tiptap/react": "^3.20.1",
|
||||
"@tiptap/starter-kit": "^3.20.1",
|
||||
"@tiptap/extension-placeholder": "^3.20.4",
|
||||
"@tiptap/markdown": "^3.20.4",
|
||||
"@tiptap/pm": "^3.20.4",
|
||||
"@tiptap/react": "^3.20.4",
|
||||
"@tiptap/starter-kit": "^3.20.4",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
|
|
@ -135,13 +135,13 @@
|
|||
"highlight.js": "^11.11.1",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"isbinaryfile": "^6.0.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"mdast-util-to-hast": "^13.2.1",
|
||||
"mermaid": "^11.12.3",
|
||||
"node-diff3": "^3.2.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-grid-layout": "^2.2.2",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable": "^3.1.3",
|
||||
|
|
@ -170,8 +170,8 @@
|
|||
"@types/hast": "^3.0.4",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^25.0.7",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitest/coverage-v8": "^3.1.4",
|
||||
|
|
|
|||
1393
pnpm-lock.yaml
1393
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { type JSX, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { isNearBottom, useAutoScrollBottom } from '@renderer/hooks/useAutoScrollBottom';
|
||||
import { useTabNavigationController } from '@renderer/hooks/useTabNavigationController';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { JSX } from 'react';
|
||||
/**
|
||||
* Empty state for ChatHistory when no conversation exists.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { type JSX } from 'react';
|
||||
|
||||
import {
|
||||
getHighlightProps,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { JSX } from 'react';
|
||||
/**
|
||||
* Loading skeleton for ChatHistory while conversation is loading.
|
||||
* Industrial shimmer with organic line widths — no generic pulse.
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ export const AssessmentBadge = ({ assessment, metricKey }: AssessmentBadgeProps)
|
|||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [tooltipPos, setTooltipPos] = useState({ top: 0, left: 0 });
|
||||
const badgeRef = useRef<HTMLSpanElement>(null);
|
||||
const enterTimer = useRef<ReturnType<typeof setTimeout>>();
|
||||
const leaveTimer = useRef<ReturnType<typeof setTimeout>>();
|
||||
const enterTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
const leaveTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (!explanation) return;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const AdvancedSection = ({
|
|||
const checkForUpdates = useStore((s) => s.checkForUpdates);
|
||||
|
||||
// Auto-revert "not-available" / "error" status back to idle after a brief display
|
||||
const revertTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const revertTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
useEffect(() => {
|
||||
if (updateStatus === 'not-available' || updateStatus === 'error') {
|
||||
revertTimerRef.current = setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ export const ConfigEditorDialog = ({
|
|||
}: ConfigEditorDialogProps): React.JSX.Element | null => {
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
const saveTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const savedRevertTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const saveTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
const savedRevertTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle');
|
||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type JSX, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { CSSProperties, MutableRefObject, PropsWithChildren, Ref } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
type JSX,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { type JSX, memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||
import { CopyButton } from '@renderer/components/common/CopyButton';
|
||||
|
|
|
|||
|
|
@ -113,7 +113,6 @@ export const MarkdownSplitView = React.memo(function MarkdownSplitView({
|
|||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Preview pane */}
|
||||
<div className="flex-1 overflow-hidden bg-surface">
|
||||
<MarkdownPreviewPane
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export const SearchInFilesPanel = ({
|
|||
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
// Monotonic request ID — prevents stale results from overwriting fresh ones
|
||||
const requestIdRef = useRef(0);
|
||||
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export const ChangeReviewDialog = ({
|
|||
const [selectionInfo, setSelectionInfo] = useState<EditorSelectionInfo | null>(null);
|
||||
const [containerRect, setContainerRect] = useState<DOMRect>(new DOMRect());
|
||||
const diffContentRef = useRef<HTMLDivElement>(null);
|
||||
const selectionTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const selectionTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
const activeSelectionFileRef = useRef<string | null>(null);
|
||||
|
||||
// EditorView map for all visible file editors
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ export const CodeMirrorDiffView = ({
|
|||
const onContentChangedRef = useRef(onContentChanged);
|
||||
const onViewChangeRef = useRef(onViewChange);
|
||||
const onSelectionChangeRef = useRef(onSelectionChange);
|
||||
const debounceTimer = useRef<ReturnType<typeof setTimeout>>();
|
||||
const debounceTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
useEffect(() => {
|
||||
onAcceptRef.current = onHunkAccepted;
|
||||
onRejectRef.current = onHunkRejected;
|
||||
|
|
|
|||
|
|
@ -59,9 +59,9 @@ interface ContinuousScrollViewProps {
|
|||
collapsedFiles?: Set<string>;
|
||||
onToggleCollapse?: (filePath: string) => void;
|
||||
onVisibleFileChange: (filePath: string) => void;
|
||||
scrollContainerRef: React.RefObject<HTMLDivElement>;
|
||||
scrollContainerRef: React.RefObject<HTMLDivElement | null>;
|
||||
editorViewMapRef: React.MutableRefObject<Map<string, EditorView>>;
|
||||
isProgrammaticScroll: React.RefObject<boolean>;
|
||||
isProgrammaticScroll: React.RefObject<boolean | null>;
|
||||
teamName: string;
|
||||
memberName: string | undefined;
|
||||
fetchFileContent: (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, type ReactNode } from 'react';
|
||||
import { Component, type JSX, type ReactNode } from 'react';
|
||||
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { type JSX, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { FileIcon } from '@renderer/components/team/editor/FileIcon';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { type JSX, useState } from 'react';
|
||||
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
import { AlertTriangle, ChevronRight, Info, ShieldCheck, X } from 'lucide-react';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
* ```
|
||||
*/
|
||||
|
||||
import { createContext, type ReactNode } from 'react';
|
||||
import { createContext, type JSX, type ReactNode } from 'react';
|
||||
|
||||
// =============================================================================
|
||||
// Context Definition
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ interface UseAutoScrollBottomOptions {
|
|||
* ref instead of creating its own. Useful when the ref needs to be shared
|
||||
* with other hooks (e.g., navigation coordinator).
|
||||
*/
|
||||
externalRef?: React.RefObject<HTMLDivElement>;
|
||||
externalRef?: React.RefObject<HTMLDivElement | null>;
|
||||
|
||||
/**
|
||||
* When this value changes, reset isAtBottom state to true.
|
||||
|
|
@ -56,7 +56,7 @@ interface UseAutoScrollBottomReturn {
|
|||
/**
|
||||
* Ref to attach to the scroll container element.
|
||||
*/
|
||||
scrollContainerRef: React.RefObject<HTMLDivElement>;
|
||||
scrollContainerRef: React.RefObject<HTMLDivElement | null>;
|
||||
|
||||
/**
|
||||
* Get whether the user is currently at the bottom of the scroll container.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ interface UseContinuousScrollNavOptions {
|
|||
|
||||
interface UseContinuousScrollNavReturn {
|
||||
scrollToFile: (filePath: string) => void;
|
||||
isProgrammaticScroll: RefObject<boolean>;
|
||||
isProgrammaticScroll: RefObject<boolean | null>;
|
||||
}
|
||||
|
||||
export function useContinuousScrollNav(
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ export function useLazyFileContent(options: UseLazyFileContentOptions): UseLazyF
|
|||
}, []);
|
||||
|
||||
// Refs for loadFile/processQueue to avoid circular useCallback deps
|
||||
const loadFileRef = useRef<(fp: string) => Promise<void>>();
|
||||
const processQueueRef = useRef<() => void>();
|
||||
const loadFileRef = useRef<(fp: string) => Promise<void>>(undefined);
|
||||
const processQueueRef = useRef<() => void>(undefined);
|
||||
|
||||
loadFileRef.current = async (filePath: string) => {
|
||||
if (!shouldLoad(filePath)) return;
|
||||
|
|
|
|||
|
|
@ -64,15 +64,15 @@ interface UseTabNavigationControllerOptions {
|
|||
/** Tab ID for consuming navigation */
|
||||
tabId: string;
|
||||
/** Refs to AI group DOM elements */
|
||||
aiGroupRefs: React.MutableRefObject<Map<string, HTMLElement>>;
|
||||
aiGroupRefs: React.RefObject<Map<string, HTMLElement>>;
|
||||
/** Refs to individual chat item DOM elements */
|
||||
chatItemRefs: React.MutableRefObject<Map<string, HTMLElement>>;
|
||||
chatItemRefs: React.RefObject<Map<string, HTMLElement>>;
|
||||
/** Refs to individual tool item DOM elements */
|
||||
toolItemRefs: React.MutableRefObject<Map<string, HTMLElement>>;
|
||||
toolItemRefs: React.RefObject<Map<string, HTMLElement>>;
|
||||
/** Function to expand an AI group (per-tab state) */
|
||||
expandAIGroup: (groupId: string) => void;
|
||||
/** Ref to scroll container */
|
||||
scrollContainerRef: React.RefObject<HTMLDivElement>;
|
||||
scrollContainerRef: React.RefObject<HTMLDivElement | null>;
|
||||
/** Height of sticky elements at top of scroll container */
|
||||
stickyOffset?: number;
|
||||
/** Optional helper to ensure a target group is mounted (e.g., virtualized lists) */
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ interface UseVisibleAIGroupOptions {
|
|||
onVisibleChange: (aiGroupId: string) => void;
|
||||
threshold?: number; // Default 0.5
|
||||
/** Optional scroll container to observe against (important for nested scroll areas). */
|
||||
rootRef?: RefObject<HTMLElement>;
|
||||
rootRef?: RefObject<HTMLElement | null>;
|
||||
}
|
||||
|
||||
interface UseVisibleAIGroupReturn {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { type RefObject, useCallback, useEffect, useRef } from 'react';
|
|||
interface UseVisibleFileSectionOptions {
|
||||
onVisibleFileChange: (filePath: string) => void;
|
||||
scrollContainerRef: RefObject<HTMLElement | null>;
|
||||
isProgrammaticScroll: RefObject<boolean>;
|
||||
isProgrammaticScroll: RefObject<boolean | null>;
|
||||
}
|
||||
|
||||
interface UseVisibleFileSectionReturn {
|
||||
|
|
@ -18,7 +18,7 @@ export function useVisibleFileSection(
|
|||
const visibleFilePaths = useRef<Set<string>>(new Set());
|
||||
const elementRefs = useRef<Map<string, HTMLElement>>(new Map());
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
const updateTopmostVisible = useCallback(() => {
|
||||
if (isProgrammaticScroll.current) return;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useVisibleAIGroup } from '../../../src/renderer/hooks/useVisibleAIGroup';
|
||||
|
|
@ -23,8 +22,9 @@ describe('useVisibleAIGroup', () => {
|
|||
});
|
||||
|
||||
it('uses provided rootRef as IntersectionObserver root', async () => {
|
||||
const observerSpy = vi.fn((cb: IntersectionObserverCallback, opts?: IntersectionObserverInit) =>
|
||||
new FakeIntersectionObserver(cb, opts)
|
||||
const observerSpy = vi.fn(
|
||||
(cb: IntersectionObserverCallback, opts?: IntersectionObserverInit) =>
|
||||
new FakeIntersectionObserver(cb, opts)
|
||||
);
|
||||
|
||||
vi.stubGlobal('IntersectionObserver', observerSpy as unknown as typeof IntersectionObserver);
|
||||
|
|
|
|||
Loading…
Reference in a new issue