From b4b9f4ea6dff62bccf2fadb2478508b5ee71ecf1 Mon Sep 17 00:00:00 2001 From: Jamison Lahman Date: Tue, 11 Aug 2026 09:15:55 -0700 Subject: [PATCH 01/26] fix(admin): reflow security page dropdowns between title and description on narrow viewports (#13847) Co-authored-by: Claude Fable 5 --- web/src/views/admin/SecurityHardeningPage.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/web/src/views/admin/SecurityHardeningPage.tsx b/web/src/views/admin/SecurityHardeningPage.tsx index 013af844177..7876ea1a84a 100644 --- a/web/src/views/admin/SecurityHardeningPage.tsx +++ b/web/src/views/admin/SecurityHardeningPage.tsx @@ -390,8 +390,9 @@ export default function SecurityHardeningPage() { title="Full User Directory Visibility" description="Exact name and email lookups work regardless of this setting." withLabel + responsive > -
+
-
+
-
+
From a7eb39c2c0875344b6ac5a1b36a2f2c4e2d9c3e3 Mon Sep 17 00:00:00 2001 From: Jamison Lahman Date: Tue, 11 Aug 2026 09:43:02 -0700 Subject: [PATCH 02/26] feat(web): allow production builds to skip type checking (#13848) Co-authored-by: Claude Sonnet 5 --- web/Dockerfile | 3 +++ web/next.config.js | 3 +++ web/package.json | 1 + 3 files changed, 7 insertions(+) diff --git a/web/Dockerfile b/web/Dockerfile index 45ce08a89b8..cfa15db7648 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -88,6 +88,9 @@ ENV SENTRY_RELEASE=${SENTRY_RELEASE} # Add NODE_OPTIONS argument ARG NODE_OPTIONS +ARG SKIP_TYPE_CHECK +ENV SKIP_TYPE_CHECK=${SKIP_TYPE_CHECK} + # SENTRY_AUTH_TOKEN is injected via BuildKit secret mount so it is never written # to any image layer, build cache, or registry manifest. # Use NODE_OPTIONS in the build command diff --git a/web/next.config.js b/web/next.config.js index 26552a56249..202fa4d1775 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -7,6 +7,9 @@ const nextConfig = { productionBrowserSourceMaps: false, poweredByHeader: false, output: "standalone", + typescript: { + ignoreBuildErrors: process.env.SKIP_TYPE_CHECK === "1", + }, transpilePackages: ["@onyx-ai/opal", "@onyx-ai/shared"], typedRoutes: true, // NOTE: `reactCompiler` is set per-phase in module.exports below — enabled for diff --git a/web/package.json b/web/package.json index 9a3576bffb9..347427942d7 100644 --- a/web/package.json +++ b/web/package.json @@ -12,6 +12,7 @@ "dev:clean": "bun run clean && next dev", "clean": "rm -rf .next node_modules/.cache *.tsbuildinfo", "build": "next build", + "build:fast": "SKIP_TYPE_CHECK=1 next build", "start": "next start", "lint": "oxlint", "lint:fix": "oxlint --fix", From 8dda38018ac55b2fcd3ce3f6098bbbababc6e92f Mon Sep 17 00:00:00 2001 From: SubashMohan Date: Tue, 11 Aug 2026 22:42:13 +0530 Subject: [PATCH 03/26] fix(highspot): surface plain-text API errors and xfail the 403 tests (#13853) Co-authored-by: Claude Opus 5 (1M context) --- backend/onyx/connectors/highspot/client.py | 6 +++++- .../highspot/test_highspot_connector.py | 20 +++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/backend/onyx/connectors/highspot/client.py b/backend/onyx/connectors/highspot/client.py index f605f4a9d36..dc9d2049bab 100644 --- a/backend/onyx/connectors/highspot/client.py +++ b/backend/onyx/connectors/highspot/client.py @@ -161,7 +161,11 @@ def _make_request( if isinstance(error_data, dict): error_msg = error_data.get("message", str(e)) except (ValueError, KeyError): - pass + # Highspot sends some errors as plain text (e.g. the licensing + # 403). Keep the body so the reason reaches the logs. + body = e.response.text.strip() + if body: + error_msg = f"{e} - {body}" if status_code == 401: raise HighspotAuthenticationError(f"Authentication failed: {error_msg}") diff --git a/backend/tests/daily/connectors/highspot/test_highspot_connector.py b/backend/tests/daily/connectors/highspot/test_highspot_connector.py index bc1d4b8bfac..4dc224c64ac 100644 --- a/backend/tests/daily/connectors/highspot/test_highspot_connector.py +++ b/backend/tests/daily/connectors/highspot/test_highspot_connector.py @@ -12,10 +12,22 @@ from onyx.connectors.models import Document, HierarchyNode from tests.utils.secret_names import TestSecret -pytestmark = pytest.mark.secrets( - TestSecret.HIGHSPOT_KEY, - TestSecret.HIGHSPOT_SECRET, -) +# Since 2026-08-10 the Highspot API answers 403 for valid credentials, so no test +# here can pass. `-x` makes the failure abort the whole connector suite, so mark +# them xfail until we know the cause. Remove the mark once the API works again. +pytestmark = [ + pytest.mark.secrets( + TestSecret.HIGHSPOT_KEY, + TestSecret.HIGHSPOT_SECRET, + ), + pytest.mark.xfail( + reason=( + "Highspot API returns 403 for valid credentials since 2026-08-10. " + "Case raised with Highspot to confirm the cause." + ), + strict=False, + ), +] def load_test_data(file_name: str = "test_highspot_data.json") -> dict: From 74a6878deb1e14a378b276ce1ba5af01b63b3220 Mon Sep 17 00:00:00 2001 From: roshan <38771624+rohoswagger@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:52:01 +0000 Subject: [PATCH 04/26] fix(admin): prevent date range picker pills from clipping in narrow headers (#13860) --- .../components/dateRangeSelectors/AdminDateRangeSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/dateRangeSelectors/AdminDateRangeSelector.tsx b/web/src/components/dateRangeSelectors/AdminDateRangeSelector.tsx index 27ef92d9ff5..b804263f248 100644 --- a/web/src/components/dateRangeSelectors/AdminDateRangeSelector.tsx +++ b/web/src/components/dateRangeSelectors/AdminDateRangeSelector.tsx @@ -88,7 +88,7 @@ export const AdminDateRangeSelector = memo(function AdminDateRangeSelector({ return (
Date: Tue, 11 Aug 2026 18:00:52 +0000 Subject: [PATCH 05/26] fix(web): stop rendering "Anonymous" while the current user is unresolved (#13852) --- web/src/lib/fetcher.ts | 13 +- web/src/providers/UserProvider.test.tsx | 172 ++++++++++++++++++ web/src/providers/UserProvider.tsx | 63 ++++++- .../skeletons/SidebarTabSkeleton.tsx | 6 +- .../sections/sidebar/AccountPopover.test.tsx | 76 ++++++++ web/src/sections/sidebar/AccountPopover.tsx | 20 +- .../setup/mocks/components/UserProvider.tsx | 2 + 7 files changed, 340 insertions(+), 12 deletions(-) create mode 100644 web/src/providers/UserProvider.test.tsx create mode 100644 web/src/sections/sidebar/AccountPopover.test.tsx diff --git a/web/src/lib/fetcher.ts b/web/src/lib/fetcher.ts index 537ccd661b4..d44ff3461ae 100644 --- a/web/src/lib/fetcher.ts +++ b/web/src/lib/fetcher.ts @@ -33,6 +33,13 @@ const DEFAULT_AUTH_ERROR_MSG = const DEFAULT_ERROR_MSG = "An error occurred while fetching the data."; +export function isAuthStatusError(error: unknown): boolean { + return ( + error instanceof FetchError && + (error.status === 401 || error.status === 402 || error.status === 403) + ); +} + /** * SWR `onErrorRetry` callback that suppresses automatic retries for * auth or tier-gated errors (401/402/403). Pass this to any SWR hook whose @@ -42,11 +49,7 @@ const DEFAULT_ERROR_MSG = "An error occurred while fetching the data."; export const skipRetryOnAuthError: NonNullable< import("swr").SWRConfiguration["onErrorRetry"] > = (error, _key, _config, revalidate, { retryCount }) => { - if ( - error instanceof FetchError && - (error.status === 401 || error.status === 402 || error.status === 403) - ) - return; + if (isAuthStatusError(error)) return; // For non-auth errors, retry with exponential backoff if ( _config.errorRetryCount !== undefined && diff --git a/web/src/providers/UserProvider.test.tsx b/web/src/providers/UserProvider.test.tsx new file mode 100644 index 00000000000..513241de874 --- /dev/null +++ b/web/src/providers/UserProvider.test.tsx @@ -0,0 +1,172 @@ +// Guards the /api/me contract: unresolved reads as loading and failed auth fetches self-heal. +import { act, render, screen } from "@tests/setup/test-utils"; +// Relative import: jest's moduleNameMapper swaps the @/ path for the global stub. +import { UserProvider, useUser } from "./UserProvider"; +import { FetchError } from "@/lib/fetcher"; +import { useCurrentUser } from "@/lib/users/hooks"; +import { User } from "@/lib/types"; + +jest.mock("posthog-js/react", () => ({ usePostHog: () => undefined })); +jest.mock("@/lib/settings/hooks", () => ({ useSettings: () => ({}) })); +jest.mock("@/lib/users/hooks", () => ({ useCurrentUser: jest.fn() })); +jest.mock("@/lib/auth/hooks", () => ({ + useAuthTypeMetadata: () => ({ + authTypeMetadata: undefined, + isLoading: false, + }), + useTokenRefresh: jest.fn(), +})); +jest.mock("@/lib/users/svc", () => ({ + updateUserPersonalization: jest.fn(), + setUserDefaultModel: jest.fn(), +})); +jest.mock("next-themes", () => ({ + useTheme: () => ({ theme: "light", setTheme: jest.fn() }), +})); + +const mockedUseCurrentUser = jest.mocked(useCurrentUser); + +function setCurrentUser(overrides: Partial>) { + mockedUseCurrentUser.mockReturnValue({ + user: undefined, + isLoading: true, + mutateUser: jest.fn(), + userError: undefined, + ...overrides, + } as ReturnType); +} + +function Probe() { + const { user, userResolution } = useUser(); + if (userResolution !== "resolved") { + return {userResolution}; + } + return {user ? user.email : "signed-out"}; +} + +// Factory: an identical element reference would let React skip the re-render. +function probeTree() { + return ( + + + + ); +} + +function renderProbe() { + return render(probeTree()); +} + +describe("UserProvider user resolution", () => { + it("reports loading while /api/me is unresolved", () => { + setCurrentUser({ user: undefined }); + renderProbe(); + expect(screen.getByText("loading")).toBeInTheDocument(); + }); + + it("reports loading when /api/me failed instead of signed-out", () => { + setCurrentUser({ + user: undefined, + isLoading: false, + userError: new Error("transient"), + }); + renderProbe(); + expect(screen.getByText("loading")).toBeInTheDocument(); + }); + + it("reports signed-out only for a resolved null user", () => { + setCurrentUser({ user: null, isLoading: false }); + renderProbe(); + expect(screen.getByText("signed-out")).toBeInTheDocument(); + }); + + it("exposes the user once resolved", () => { + setCurrentUser({ + user: { id: "u1", email: "john@example.com" } as User, + isLoading: false, + }); + renderProbe(); + expect(screen.getByText("john@example.com")).toBeInTheDocument(); + }); +}); + +function authError(label: string): FetchError { + return new FetchError(label, 403, null); +} + +describe("UserProvider /api/me retry", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("retries an auth failure on a backoff schedule, then reports unavailable", () => { + const mutateUser = jest.fn(); + + setCurrentUser({ userError: authError("fail 1"), mutateUser }); + const { rerender } = renderProbe(); + act(() => jest.advanceTimersByTime(2_000)); + expect(mutateUser).toHaveBeenCalledTimes(1); + + setCurrentUser({ userError: authError("fail 2"), mutateUser }); + rerender(probeTree()); + act(() => jest.advanceTimersByTime(5_000)); + expect(mutateUser).toHaveBeenCalledTimes(2); + + setCurrentUser({ userError: authError("fail 3"), mutateUser }); + rerender(probeTree()); + act(() => jest.advanceTimersByTime(15_000)); + expect(mutateUser).toHaveBeenCalledTimes(3); + + setCurrentUser({ userError: authError("fail 4"), mutateUser }); + rerender(probeTree()); + act(() => jest.advanceTimersByTime(120_000)); + expect(mutateUser).toHaveBeenCalledTimes(3); + // Budget spent: unavailable, never a false signed-out claim. + expect(screen.getByText("unavailable")).toBeInTheDocument(); + }); + + it("does not schedule retries for non-auth failures (SWR owns those)", () => { + const mutateUser = jest.fn(); + + setCurrentUser({ + userError: new FetchError("boom", 500, null), + mutateUser, + }); + renderProbe(); + act(() => jest.advanceTimersByTime(29_000)); + expect(mutateUser).not.toHaveBeenCalled(); + expect(screen.getByText("loading")).toBeInTheDocument(); + }); + + it("reports unavailable once a non-auth failure outlives the deadline", () => { + setCurrentUser({ userError: new FetchError("boom", 500, null) }); + renderProbe(); + act(() => jest.advanceTimersByTime(31_000)); + expect(screen.getByText("unavailable")).toBeInTheDocument(); + }); + + it("resets the retry budget after a successful fetch", () => { + const mutateUser = jest.fn(); + + setCurrentUser({ userError: authError("fail"), mutateUser }); + const { rerender } = renderProbe(); + act(() => jest.advanceTimersByTime(2_000)); + expect(mutateUser).toHaveBeenCalledTimes(1); + + setCurrentUser({ + user: { id: "u1", email: "john@example.com" } as User, + isLoading: false, + mutateUser, + }); + rerender(probeTree()); + + setCurrentUser({ userError: authError("fail again"), mutateUser }); + rerender(probeTree()); + act(() => jest.advanceTimersByTime(2_000)); + expect(mutateUser).toHaveBeenCalledTimes(2); + }); +}); diff --git a/web/src/providers/UserProvider.tsx b/web/src/providers/UserProvider.tsx index 2950afe4547..dd5094480a1 100644 --- a/web/src/providers/UserProvider.tsx +++ b/web/src/providers/UserProvider.tsx @@ -16,6 +16,7 @@ import { ThemePreference, } from "@/lib/types"; import { usePostHog } from "posthog-js/react"; +import { isAuthStatusError } from "@/lib/fetcher"; import { useSettings } from "@/lib/settings/hooks"; import { useCurrentUser } from "@/lib/users/hooks"; import { useAuthTypeMetadata, useTokenRefresh } from "@/lib/auth/hooks"; @@ -26,8 +27,18 @@ import { } from "@/lib/users/svc"; import { useTheme } from "next-themes"; +// Auth failures skip SWR's retry but are usually transient refresh races. SWR's backoff owns the rest. +const ME_RETRY_DELAYS_MS = [2_000, 5_000, 15_000]; + +// Ceiling on reporting loading for a failing /api/me, so the account menu always comes back. +const ME_LOADING_DEADLINE_MS = 30_000; + +/** Only "resolved" lets a null user mean signed out. "unavailable" means /api/me keeps failing for a possibly valid session. */ +export type UserResolution = "loading" | "unavailable" | "resolved"; + interface UserContextType { user: User | null; + userResolution: UserResolution; isAdmin: boolean; isCurator: boolean; refreshUser: () => Promise; @@ -61,7 +72,7 @@ interface UserContextType { const UserContext = createContext(undefined); export function UserProvider({ children }: { children: React.ReactNode }) { - const { user: fetchedUser, mutateUser } = useCurrentUser(); + const { user: fetchedUser, mutateUser, userError } = useCurrentUser(); const { authTypeMetadata, isLoading: authTypeMetadataLoading } = useAuthTypeMetadata(); const updatedSettingsData = useSettings(); @@ -99,6 +110,55 @@ export function UserProvider({ children }: { children: React.ReactNode }) { setUpToDateUser(mergeUserPreferences(fetchedUser ?? null)); }, [fetchedUser, mergeUserPreferences]); + const [meRetriesExhausted, setMeRetriesExhausted] = useState(false); + + const meRetryCountRef = useRef(0); + const meFirstErrorAtRef = useRef(null); + useEffect(() => { + if (!userError) { + meRetryCountRef.current = 0; + meFirstErrorAtRef.current = null; + setMeRetriesExhausted(false); + return; + } + meFirstErrorAtRef.current ??= Date.now(); + if (!isAuthStatusError(userError)) { + // SWR's backoff owns non-auth retries. Bound only how long we report loading. + const remaining = + ME_LOADING_DEADLINE_MS - (Date.now() - meFirstErrorAtRef.current); + if (remaining <= 0) { + setMeRetriesExhausted(true); + return; + } + const deadlineId = setTimeout( + () => setMeRetriesExhausted(true), + remaining + ); + return () => clearTimeout(deadlineId); + } + // Fresh error identity per failure advances the schedule. The count moves in the timer, so StrictMode is safe. + const attempt = meRetryCountRef.current; + const delay = ME_RETRY_DELAYS_MS[attempt]; + if (delay === undefined) { + setMeRetriesExhausted(true); + return; + } + const timeoutId = setTimeout(() => { + meRetryCountRef.current = attempt + 1; + void mutateUser(); + }, delay); + return () => clearTimeout(timeoutId); + }, [userError, mutateUser]); + + const awaitingMe = fetchedUser === undefined && !meRetriesExhausted; + const mergePending = fetchedUser != null && upToDateUser === null; + const userResolution: UserResolution = + awaitingMe || mergePending + ? "loading" + : fetchedUser === undefined + ? "unavailable" + : "resolved"; + useEffect(() => { if (!posthog) return; @@ -539,6 +599,7 @@ export function UserProvider({ children }: { children: React.ReactNode }) {
diff --git a/web/src/sections/sidebar/AccountPopover.test.tsx b/web/src/sections/sidebar/AccountPopover.test.tsx new file mode 100644 index 00000000000..caec42688a5 --- /dev/null +++ b/web/src/sections/sidebar/AccountPopover.test.tsx @@ -0,0 +1,76 @@ +// The chip shows a skeleton while unresolved and "Anonymous" only when resolved signed-out. +import { render, screen } from "@tests/setup/test-utils"; +import AccountPopover from "@/sections/sidebar/AccountPopover"; +import { useUser } from "@/providers/UserProvider"; +import { User } from "@/lib/types"; + +// Factory mock: the global stub pins userResolution to "resolved". +jest.mock("@/providers/UserProvider", () => ({ useUser: jest.fn() })); +jest.mock("next/navigation", () => ({ + useRouter: () => ({ push: jest.fn() }), + usePathname: () => "/app", + useSearchParams: () => new URLSearchParams(), +})); +jest.mock("@/sections/sidebar/NotificationsPopover", () => ({ + __esModule: true, + default: () => null, +})); +jest.mock("@/hooks/useAppFocus", () => ({ + __esModule: true, + default: () => ({ isUserSettings: () => false }), +})); +jest.mock("@/hooks/useScreenSize", () => ({ + __esModule: true, + default: () => ({ isMobile: false }), +})); +jest.mock("@/lib/settings/hooks", () => ({ + useSettings: () => ({ vectorDbEnabled: false }), +})); +jest.mock("@/hooks/useNotifications", () => ({ + useNotificationSummary: () => ({ undismissedCount: 0, refresh: jest.fn() }), +})); + +const mockedUseUser = jest.mocked(useUser); + +function setUser( + user: User | null, + userResolution: "loading" | "unavailable" | "resolved" +) { + mockedUseUser.mockReturnValue({ + user, + userResolution, + } as ReturnType); +} + +it("shows a skeleton instead of Anonymous while the user is unresolved", () => { + setUser(null, "loading"); + render(); + expect(screen.queryByText("Anonymous")).not.toBeInTheDocument(); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); +}); + +it("shows a neutral label, not Anonymous, when the user is unavailable", () => { + setUser(null, "unavailable"); + render(); + expect(screen.getByText("Account")).toBeInTheDocument(); + expect(screen.queryByText("Anonymous")).not.toBeInTheDocument(); +}); + +it("shows Anonymous for a resolved signed-out user", () => { + setUser(null, "resolved"); + render(); + expect(screen.getByText("Anonymous")).toBeInTheDocument(); +}); + +it("shows the user's name once resolved", () => { + setUser( + { + id: "u1", + email: "john@example.com", + personalization: { name: "John" }, + } as unknown as User, + "resolved" + ); + render(); + expect(screen.getByText("John")).toBeInTheDocument(); +}); diff --git a/web/src/sections/sidebar/AccountPopover.tsx b/web/src/sections/sidebar/AccountPopover.tsx index a486aeaebdf..58fcde41861 100644 --- a/web/src/sections/sidebar/AccountPopover.tsx +++ b/web/src/sections/sidebar/AccountPopover.tsx @@ -30,6 +30,7 @@ import useAppFocus from "@/hooks/useAppFocus"; import useScreenSize from "@/hooks/useScreenSize"; import { useSettings } from "@/lib/settings/hooks"; import UserAvatar from "@/refresh-components/avatars/UserAvatar"; +import SidebarTabSkeleton from "@/refresh-components/skeletons/SidebarTabSkeleton"; import { useNotificationSummary } from "@/hooks/useNotifications"; import { SvgOnyxLogo } from "@opal/logos"; import { markdown } from "@opal/utils"; @@ -45,7 +46,7 @@ function SettingsPopover({ onOpenNotifications, undismissedCount, }: SettingsPopoverProps) { - const { user } = useUser(); + const { user, userResolution } = useUser(); const settings = useSettings(); const enterpriseSettings = settings.enterprise; const router = useRouter(); @@ -90,7 +91,14 @@ function SettingsPopover({ {[
- +
, null,
@@ -198,13 +206,14 @@ export default function AccountPopover({ const [popupState, setPopupState] = useState< "Settings" | "Notifications" | undefined >(undefined); - const { user } = useUser(); + const { user, userResolution } = useUser(); const appFocus = useAppFocus(); const { isMobile } = useScreenSize(); const { vectorDbEnabled } = useSettings(); const { undismissedCount, refresh: refreshNotificationSummary } = useNotificationSummary(); - const userDisplayName = getUserDisplayName(user); + const userDisplayName = + userResolution === "unavailable" ? "Account" : getUserDisplayName(user); const handlePopoverOpen = (state: boolean) => { if (state) { @@ -221,6 +230,9 @@ export default function AccountPopover({ setPopupState(undefined); } }; + if (userResolution === "loading") { + return ; + } return ( diff --git a/web/tests/setup/mocks/components/UserProvider.tsx b/web/tests/setup/mocks/components/UserProvider.tsx index ccf8d652cb8..e4916f20c79 100644 --- a/web/tests/setup/mocks/components/UserProvider.tsx +++ b/web/tests/setup/mocks/components/UserProvider.tsx @@ -19,6 +19,7 @@ import React, { createContext, useContext } from "react"; interface UserContextType { user: any; + userResolution: "loading" | "unavailable" | "resolved"; isAdmin: boolean; isCurator: boolean; refreshUser: () => Promise; @@ -36,6 +37,7 @@ interface UserContextType { const mockUserContext: UserContextType = { user: null, + userResolution: "resolved", isAdmin: false, isCurator: false, refreshUser: async () => {}, From e2125952f0bacdc02e3e8a879edf21e33d4a999d Mon Sep 17 00:00:00 2001 From: Wenxi Date: Tue, 11 Aug 2026 18:16:35 +0000 Subject: [PATCH 06/26] fix(craft): restore stale skills after turn failure (#13859) --- .../components/SkillsStaleNotice.test.tsx | 14 ++++- .../craft/components/SkillsStaleNotice.tsx | 11 +--- .../craft/hooks/loadSessionRestore.test.ts | 56 ++++++++++++++++++- .../hooks/useBuildSessionController.test.tsx | 10 ++-- .../craft/hooks/useBuildSessionController.ts | 3 +- .../app/craft/hooks/useBuildSessionStore.ts | 17 ++++-- .../craft/hooks/useBuildStreaming.test.tsx | 39 +++++++++++++ web/src/app/craft/hooks/useBuildStreaming.ts | 7 +++ 8 files changed, 135 insertions(+), 22 deletions(-) diff --git a/web/src/app/craft/components/SkillsStaleNotice.test.tsx b/web/src/app/craft/components/SkillsStaleNotice.test.tsx index f061ca1ee1d..85872df7abf 100644 --- a/web/src/app/craft/components/SkillsStaleNotice.test.tsx +++ b/web/src/app/craft/components/SkillsStaleNotice.test.tsx @@ -36,9 +36,17 @@ describe("SkillsStaleNotice", () => { }); }); - it("disables reload while a turn is active", () => { - render(); + it("reveals retained stale state after an active turn stops", () => { + const { rerender } = render( + + ); - expect(screen.getByRole("button", { name: "Reload" })).toBeDisabled(); + expect( + screen.queryByRole("button", { name: "Reload" }) + ).not.toBeInTheDocument(); + + rerender(); + + expect(screen.getByRole("button", { name: "Reload" })).toBeEnabled(); }); }); diff --git a/web/src/app/craft/components/SkillsStaleNotice.tsx b/web/src/app/craft/components/SkillsStaleNotice.tsx index 3b22a21f32b..ecfdc6dabc6 100644 --- a/web/src/app/craft/components/SkillsStaleNotice.tsx +++ b/web/src/app/craft/components/SkillsStaleNotice.tsx @@ -34,20 +34,15 @@ export default function SkillsStaleNotice({ } }; + if (turnActive) return null; + return ( + } diff --git a/web/src/app/craft/hooks/loadSessionRestore.test.ts b/web/src/app/craft/hooks/loadSessionRestore.test.ts index c1f5c21a133..8e4092f6c7f 100644 --- a/web/src/app/craft/hooks/loadSessionRestore.test.ts +++ b/web/src/app/craft/hooks/loadSessionRestore.test.ts @@ -12,10 +12,11 @@ const SESSION_ID = "11111111-1111-1111-1111-111111111111"; // Minimal DetailedSessionResponse shapes — loadSession only reads status, // session_loaded_in_sandbox, nextjs_port, and sandbox.status. -function sleepingSession(): unknown { +function sleepingSession(): Record { return { id: SESSION_ID, status: "idle", + skills_stale: false, nextjs_port: null, session_loaded_in_sandbox: false, sandbox: { id: "sb1", status: "sleeping" }, @@ -28,6 +29,7 @@ function runningSession( return { id: SESSION_ID, status: "active", + skills_stale: false, nextjs_port: nextjsPort, session_loaded_in_sandbox: true, sandbox: { id: "sb1", status: "running" }, @@ -38,6 +40,14 @@ function webappInfo(has_webapp: boolean | null, ready: boolean): unknown { return { has_webapp, webapp_url: null, status: "running", ready }; } +function deferred(): { promise: Promise; resolve: (value: T) => void } { + let resolve: (value: T) => void = () => {}; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + describe("loadSession restore status", () => { beforeEach(() => { jest.clearAllMocks(); @@ -88,6 +98,23 @@ describe("loadSession restore status", () => { expect(session?.sandbox?.status).toBe("failed"); }); + it("clears stale skills after a successful session restore", async () => { + mockedApi.fetchSession.mockResolvedValue({ + ...sleepingSession(), + skills_stale: true, + } as never); + mockedApi.restoreSession.mockResolvedValue({ + ...runningSession(), + skills_stale: false, + } as never); + + await useBuildSessionStore.getState().loadSession(SESSION_ID); + + expect( + useBuildSessionStore.getState().sessions.get(SESSION_ID)?.skillsStale + ).toBe(false); + }); + it("waits for the webapp before flipping to running, then shows running", async () => { mockedApi.fetchSession.mockResolvedValue(sleepingSession() as never); mockedApi.restoreSession.mockResolvedValue(runningSession() as never); @@ -471,7 +498,7 @@ describe("loadSession restore status", () => { expect(session?.activeTurnLocalOwner).toBe(false); }); - it("preserves stale-skill state while loading a pre-provisioned turn", async () => { + it("retains fetched stale-skill state during a pre-provisioned turn", async () => { mockedApi.fetchSession.mockResolvedValue({ ...runningSession(), skills_stale: true, @@ -494,6 +521,31 @@ describe("loadSession restore status", () => { .getState() .loadSession(SESSION_ID, { force: true }); + expect( + useBuildSessionStore.getState().sessions.get(SESSION_ID)?.skillsStale + ).toBe(true); + }); + + it("rejects a load fetched before a newer stale-skill update", async () => { + const messages = deferred(); + mockedApi.fetchSession.mockResolvedValue({ + ...runningSession(), + skills_stale: true, + } as never); + mockedApi.fetchMessages.mockReturnValue(messages.promise as never); + + const load = useBuildSessionStore + .getState() + .loadSession(SESSION_ID, { force: true }); + await Promise.resolve(); + expect(mockedApi.fetchMessages).toHaveBeenCalled(); + + useBuildSessionStore + .getState() + .updateSessionData(SESSION_ID, { skillsStale: false }); + messages.resolve([]); + await load; + expect( useBuildSessionStore.getState().sessions.get(SESSION_ID)?.skillsStale ).toBe(false); diff --git a/web/src/app/craft/hooks/useBuildSessionController.test.tsx b/web/src/app/craft/hooks/useBuildSessionController.test.tsx index bcc07d6212d..364361d3177 100644 --- a/web/src/app/craft/hooks/useBuildSessionController.test.tsx +++ b/web/src/app/craft/hooks/useBuildSessionController.test.tsx @@ -41,7 +41,7 @@ describe("useBuildSessionController", () => { useBuildSessionStore.getState().setCurrentSession(SESSION_ID); }); - it("refreshes stale skill state on mount and browser focus", async () => { + it("marks skills stale from reads without clearing confirmed stale state", async () => { jest.mocked(api.fetchSession).mockResolvedValue({ skills_stale: true, } as never); @@ -65,10 +65,12 @@ describe("useBuildSessionController", () => { act(() => window.dispatchEvent(new Event("focus"))); await waitFor(() => { - expect( - useBuildSessionStore.getState().sessions.get(SESSION_ID)?.skillsStale - ).toBe(false); + expect(api.fetchSession).toHaveBeenCalledTimes(2); }); + await act(async () => Promise.resolve()); + expect( + useBuildSessionStore.getState().sessions.get(SESSION_ID)?.skillsStale + ).toBe(true); }); it("does not restore stale state after an intervening reload", async () => { diff --git a/web/src/app/craft/hooks/useBuildSessionController.ts b/web/src/app/craft/hooks/useBuildSessionController.ts index 5df7d7992b8..d33b57ca6d1 100644 --- a/web/src/app/craft/hooks/useBuildSessionController.ts +++ b/web/src/app/craft/hooks/useBuildSessionController.ts @@ -109,12 +109,13 @@ export function useBuildSessionController({ const session = await fetchSession(sessionId, { checkWorkspace: false, }); + if (!session.skills_stale) return; const currentSession = useBuildSessionStore .getState() .sessions.get(sessionId); if (currentSession?.skillsStaleRevision !== skillsStaleRevision) return; updateSessionData(sessionId, { - skillsStale: session.skills_stale, + skillsStale: true, }); } catch { // Keep the usable cached session on transient refresh failures. diff --git a/web/src/app/craft/hooks/useBuildSessionStore.ts b/web/src/app/craft/hooks/useBuildSessionStore.ts index 468062ea779..147e77a174d 100644 --- a/web/src/app/craft/hooks/useBuildSessionStore.ts +++ b/web/src/app/craft/hooks/useBuildSessionStore.ts @@ -1506,6 +1506,11 @@ export const useBuildSessionStore = create()((set, get) => ({ // Set as current and mark as loading setCurrentSession(sessionId); + const skillsStaleRevision = + get().sessions.get(sessionId)!.skillsStaleRevision; + const canApplySkillsStale = () => + get().sessions.get(sessionId)?.skillsStaleRevision === + skillsStaleRevision; try { // First fetch session to check sandbox status @@ -1611,9 +1616,8 @@ export const useBuildSessionStore = create()((set, get) => ({ sandbox, agentProvider: sessionData.agent_provider, agentModel: sessionData.agent_model, - // Persisted loads reconcile stale state. Optimistic welcome loads keep - // their live local state until the turn settles. - ...(useDbMessages && { skillsStale: sessionData.skills_stale }), + ...(sessionData.skills_stale && + canApplySkillsStale() && { skillsStale: true }), origin: sessionData.origin, activeTurnId: resolvedActiveTurnId, activeTurnIndex: resolvedActiveTurnIndex, @@ -1628,6 +1632,8 @@ export const useBuildSessionStore = create()((set, get) => ({ }); if (needsRestore) { + const skillsStaleRevisionBeforeRestore = + get().sessions.get(sessionId)?.skillsStaleRevision; try { sessionData = await restoreSession(sessionId); } catch (restoreErr) { @@ -1649,7 +1655,10 @@ export const useBuildSessionStore = create()((set, get) => ({ sandbox: sessionData.sandbox ? { ...sessionData.sandbox, status: "restoring" } : sessionData.sandbox, - skillsStale: sessionData.skills_stale, + ...(get().sessions.get(sessionId)?.skillsStaleRevision === + skillsStaleRevisionBeforeRestore && { + skillsStale: sessionData.skills_stale, + }), webappNeedsRefresh: (get().sessions.get(sessionId)?.webappNeedsRefresh || 0) + 1, }); diff --git a/web/src/app/craft/hooks/useBuildStreaming.test.tsx b/web/src/app/craft/hooks/useBuildStreaming.test.tsx index 5d3c1486879..a10ee7fbf57 100644 --- a/web/src/app/craft/hooks/useBuildStreaming.test.tsx +++ b/web/src/app/craft/hooks/useBuildStreaming.test.tsx @@ -119,6 +119,45 @@ describe("useBuildStreaming thinking packets", () => { }); }); + it.each([ + { changesDuringTurn: false, expected: false }, + { changesDuringTurn: true, expected: true }, + ])( + "reconciles stale skills after turn creation when changesDuringTurn is $changesDuringTurn", + async ({ changesDuringTurn, expected }) => { + let resolveTurn: ( + turn: Awaited> + ) => void = () => {}; + jest.mocked(createTurn).mockReturnValueOnce( + new Promise((resolve) => { + resolveTurn = resolve; + }) + ); + useBuildSessionStore.getState().updateSessionData(sessionId, { + skillsStale: true, + }); + const { result } = renderHook(() => useBuildStreaming()); + + const stream = result.current.streamMessage(sessionId, "build the app"); + if (changesDuringTurn) { + useBuildSessionStore.getState().updateSessionData(sessionId, { + skillsStale: true, + }); + } + resolveTurn({ + session_id: sessionId, + turn_id: "turn-thinking", + status: "QUEUED", + turn_index: 0, + }); + await act(async () => stream); + + expect( + useBuildSessionStore.getState().sessions.get(sessionId)?.skillsStale + ).toBe(expected); + } + ); + it("does not reset the abort controller when a newer turn took ownership mid-stream", async () => { const newerController = new AbortController(); jest.mocked(processSSEStream).mockImplementationOnce(async () => { diff --git a/web/src/app/craft/hooks/useBuildStreaming.ts b/web/src/app/craft/hooks/useBuildStreaming.ts index a379d6d2a0a..53c86451469 100644 --- a/web/src/app/craft/hooks/useBuildStreaming.ts +++ b/web/src/app/craft/hooks/useBuildStreaming.ts @@ -957,6 +957,7 @@ export function useBuildStreaming() { ): Promise => { const currentState = useBuildSessionStore.getState(); const existingSession = currentState.sessions.get(sessionId); + const skillsStaleRevision = existingSession?.skillsStaleRevision; if (existingSession?.abortController) { existingSession.abortController.abort(); @@ -986,10 +987,16 @@ export function useBuildStreaming() { model, attachments ); + const currentSession = useBuildSessionStore + .getState() + .sessions.get(sessionId); updateSessionData(sessionId, { activeTurnId: turn.turn_id, activeTurnIndex: turn.turn_index, activeTurnLocalOwner: true, + ...(currentSession?.skillsStaleRevision === skillsStaleRevision && { + skillsStale: false, + }), }); await streamTurnEvents(sessionId, turn.turn_id, controller.signal); From 31aa7b1161909d3805362e3cf8194f2ed3212dd7 Mon Sep 17 00:00:00 2001 From: Evan Lohn Date: Tue, 11 Aug 2026 18:42:13 +0000 Subject: [PATCH 07/26] fix: preserve HTML table and link boundaries (#13864) --- backend/onyx/file_processing/html_utils.py | 40 +++++---- .../confluence/test_confluence_html_parser.py | 83 +++++++++++++++++++ .../cross_connector_utils/test_html_utils.py | 36 ++++++++ 3 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 backend/tests/daily/connectors/confluence/test_confluence_html_parser.py diff --git a/backend/onyx/file_processing/html_utils.py b/backend/onyx/file_processing/html_utils.py index 57c7d9d106b..fd0f9971e68 100644 --- a/backend/onyx/file_processing/html_utils.py +++ b/backend/onyx/file_processing/html_utils.py @@ -18,6 +18,9 @@ logger = setup_logger() MINTLIFY_UNWANTED = ["sticky", "hidden"] +_ANCHOR_ELEMENT = "a" +_HREF_ATTRIBUTE = "href" +_TABLE_ELEMENT = "table" @dataclass @@ -54,6 +57,20 @@ def format_element_text(element_text: str, link_href: str | None) -> str: return f"[{element_text_no_newlines}]({link_href})" +def _get_ancestor_link_href( + element: bs4.element.PageElement, in_table: bool +) -> str | None: + if in_table: + return None + + link = element.find_parent(_ANCHOR_ELEMENT) + if not link: + return None + + href = link.get(_HREF_ATTRIBUTE) + return href[0] if isinstance(href, list) else href + + def parse_html_with_trafilatura(html_content: str) -> str: """Parse HTML content using trafilatura.""" import trafilatura @@ -84,15 +101,14 @@ def format_document_soup( text = "" list_element_start = False verbatim_output = 0 - in_table = False last_added_newline = False - link_href: str | None = None for e in document.descendants: verbatim_output -= 1 if isinstance(e, bs4.element.NavigableString): if isinstance(e, (bs4.element.Comment, bs4.element.Doctype)): continue + in_table = e.find_parent(_TABLE_ELEMENT) is not None element_text = e.text if in_table: # Tables are represented in natural language with rows separated by newlines @@ -110,7 +126,9 @@ def format_document_soup( content_to_add = ( element_text if verbatim_output > 0 - else format_element_text(element_text, link_href) + else format_element_text( + element_text, _get_ancestor_link_href(e, in_table) + ) ) # Don't join separate elements without any spacing @@ -123,28 +141,16 @@ def format_document_soup( list_element_start = False elif isinstance(e, bs4.element.Tag): - # table is standard HTML element - if e.name == "table": - in_table = True + in_table = e.find_parent(_TABLE_ELEMENT) is not None # tr is for rows - elif e.name == "tr" and in_table: + if e.name == "tr" and in_table: text += "\n" # td for data cell, th for header elif e.name in ["td", "th"] and in_table: text += table_cell_separator - elif e.name == "/table": - in_table = False elif in_table: # don't handle other cases while in table pass - elif e.name == "a": - href_value = e.get("href", None) - # mostly for typing, having multiple hrefs is not valid HTML - link_href = ( - href_value[0] if isinstance(href_value, list) else href_value - ) - elif e.name == "/a": - link_href = None elif e.name in ["p", "div"]: if not list_element_start: text += "\n" diff --git a/backend/tests/daily/connectors/confluence/test_confluence_html_parser.py b/backend/tests/daily/connectors/confluence/test_confluence_html_parser.py new file mode 100644 index 00000000000..213ce5eca35 --- /dev/null +++ b/backend/tests/daily/connectors/confluence/test_confluence_html_parser.py @@ -0,0 +1,83 @@ +import os +import time +from unittest.mock import patch + +import pytest + +from onyx.configs.constants import DocumentSource +from onyx.connectors.confluence.connector import ConfluenceConnector +from onyx.connectors.credentials_provider import OnyxStaticCredentialsProvider +from onyx.connectors.models import Document +from onyx.file_processing.enums import HtmlBasedConnectorTransformLinksStrategy +from tests.daily.connectors.utils import load_all_from_connector +from tests.utils.secret_names import TestSecret + +_PARSER_REGRESSION_SPACE = "ParserReg" +_LINK_PAGE_TITLE = "HTML Parser Regression - Link Scope" +_TABLE_PAGE_TITLE = "HTML Parser Regression - Table Scope" +_LINK_TARGET = "https://example.com/parser-regression-target" + +pytestmark = pytest.mark.secrets(TestSecret.CONFLUENCE_ACCESS_TOKEN) + + +def _make_connector(access_token: str) -> ConfluenceConnector: + connector = ConfluenceConnector( + wiki_base=os.environ["CONFLUENCE_TEST_SPACE_URL"], + space=_PARSER_REGRESSION_SPACE, + is_cloud=True, + ) + connector.set_credentials_provider( + OnyxStaticCredentialsProvider( + None, + DocumentSource.CONFLUENCE, + { + "confluence_username": os.environ["CONFLUENCE_USER_NAME"], + "confluence_access_token": access_token, + }, + ) + ) + return connector + + +@pytest.fixture(scope="module") +def parser_regression_documents( + test_secrets: dict[TestSecret, str], +) -> dict[str, Document]: + connector = _make_connector( + test_secrets[TestSecret.CONFLUENCE_ACCESS_TOKEN].strip() + ) + with patch( + "onyx.file_processing.html_utils.HTML_BASED_CONNECTOR_TRANSFORM_LINKS_STRATEGY", + HtmlBasedConnectorTransformLinksStrategy.MARKDOWN, + ): + result = load_all_from_connector(connector, 0, time.time()) + + return {document.semantic_identifier: document for document in result.documents} + + +def test_confluence_html_link_scope( + parser_regression_documents: dict[str, Document], +) -> None: + document = parser_regression_documents[_LINK_PAGE_TITLE] + + assert document.sections[0].text == ( + f"LINK_BEFORE [LINK_TARGET_ONLY]({_LINK_TARGET}) " + "LINK_AFTER_MUST_NOT_BE_CLICKABLE\n" + "LINK_NEXT_PARAGRAPH_MUST_NOT_BE_CLICKABLE" + ) + + +def test_confluence_html_table_scope( + parser_regression_documents: dict[str, Document], +) -> None: + document = parser_regression_documents[_TABLE_PAGE_TITLE] + + assert document.sections[0].text == ( + "TABLE_BEFORE\n" + "\tHEADER_ALPHA\tHEADER_BETA\n" + "\tCELL_ALPHA\tCELL_BETA_LINE_1 CELL_BETA_LINE_2\n" + "TABLE_AFTER_HEADING\n" + "TABLE_AFTER_PARAGRAPH_MUST_BE_SEPARATE\n" + "- TABLE_AFTER_LIST_ONE\n" + "- TABLE_AFTER_LIST_TWO" + ) diff --git a/backend/tests/unit/onyx/connectors/cross_connector_utils/test_html_utils.py b/backend/tests/unit/onyx/connectors/cross_connector_utils/test_html_utils.py index 9ddc1941bb5..7c203b4d750 100644 --- a/backend/tests/unit/onyx/connectors/cross_connector_utils/test_html_utils.py +++ b/backend/tests/unit/onyx/connectors/cross_connector_utils/test_html_utils.py @@ -1,5 +1,9 @@ import pathlib +import pytest + +import onyx.file_processing.html_utils as html_utils +from onyx.file_processing.enums import HtmlBasedConnectorTransformLinksStrategy from onyx.file_processing.html_utils import parse_html_page_basic @@ -11,3 +15,35 @@ def test_parse_table() -> None: parsed = parse_html_page_basic(content) expected = "\n\thello\tthere\tgeneral\n\tkenobi\ta\tb\n\tc\td\te" assert expected in parsed + + +def test_content_after_table_uses_normal_block_formatting() -> None: + html = ( + "

before

" + "
cell
" + "

after heading

" + "

after paragraph

" + "
  • one
  • two
" + ) + + assert parse_html_page_basic(html) == ( + "before\n\tcell\nafter heading\nafter paragraph\n- one\n- two" + ) + + +def test_markdown_link_ends_at_anchor( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + html_utils, + "HTML_BASED_CONNECTOR_TRANSFORM_LINKS_STRATEGY", + HtmlBasedConnectorTransformLinksStrategy.MARKDOWN, + ) + html = ( + '

See this link now.

' + "

Next paragraph.

" + ) + + assert parse_html_page_basic(html) == ( + "See [this link](https://example.com) now.\nNext paragraph." + ) From 23dbf48fb8d7a81ea8703a49a980787baa744cd5 Mon Sep 17 00:00:00 2001 From: Nikolas Garza <90273783+nmgarza5@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:45:27 +0000 Subject: [PATCH 08/26] fix(web): fix react-doctor error-class findings and enforce via oxlint plugin (#13850) --- web/.oxlintrc.json | 13 +- web/bun.lock | 181 ++++++++++---- .../inputs/input-select/components.tsx | 13 +- .../opal/src/components/modal/components.tsx | 34 +-- .../components/table/hooks/useDataTable.ts | 8 +- web/lib/opal/src/components/tabs/hooks.ts | 3 + web/lib/opal/src/layouts/root/components.tsx | 4 +- web/package.json | 1 + .../ConnectorInput/StringPairListInput.tsx | 47 +++- web/src/app/app/message/HumanMessage.tsx | 2 - .../app/message/MultiModelResponseView.tsx | 53 +++-- .../messageComponents/hooks/useAuthErrors.ts | 66 +++--- .../renderers/MessageTextRenderer.tsx | 2 + .../timeline/hooks/usePacedTurnGroups.ts | 98 +++++--- .../timeline/hooks/usePacketProcessor.ts | 3 + .../renderers/reasoning/ReasoningRenderer.tsx | 44 ++-- .../app/craft/components/BuildMessageList.tsx | 2 + web/src/app/craft/components/ChatPanel.tsx | 14 +- .../craft/components/OpencodeDebugLogs.tsx | 4 +- web/src/app/craft/contexts/BuildContext.tsx | 10 +- .../app/craft/contexts/UploadFilesContext.tsx | 63 ++--- .../v1/tasks/components/ScheduleTaskForm.tsx | 38 +-- web/src/app/ee/admin/export-logs/page.tsx | 28 ++- web/src/app/ee/admin/standard-answer/page.tsx | 2 - web/src/components/voice/Waveform.tsx | 5 +- .../ee/providers/QueryControllerProvider.tsx | 15 +- web/src/ee/sections/SearchUI.tsx | 5 +- web/src/hooks/useContainerCenter.ts | 67 +++--- web/src/hooks/useContentSize.ts | 27 ++- web/src/hooks/useDraft.ts | 7 +- web/src/hooks/useEscapeInterrupt.ts | 5 +- web/src/hooks/useSlashPicker.ts | 13 +- web/src/hooks/useTypewriter.ts | 9 +- web/src/hooks/useUnsavedChangesGuard.ts | 5 +- web/src/hooks/useVisibilityGatedInterval.ts | 5 +- web/src/lib/agents/hooks.ts | 6 +- web/src/lib/auth/hooks.ts | 40 ++-- web/src/lib/hooks.ts | 91 +++---- web/src/lib/hooks/useCaptcha.ts | 38 ++- web/src/providers/FullWidthChatProvider.tsx | 18 +- web/src/providers/ProjectsContext.tsx | 38 ++- web/src/providers/VoiceModeProvider.tsx | 222 ++++++++++++------ .../refresh-components/inputs/InputSelect.tsx | 29 +-- .../popovers/ActionsPopover/index.tsx | 60 +++-- .../actions/modals/MCPAuthenticationModal.tsx | 43 ++-- web/src/sections/chat/ChatScrollContainer.tsx | 15 +- web/src/sections/chat/ChatUI.tsx | 13 +- web/src/sections/input/AppInputBar.tsx | 18 +- web/src/sections/input/EntryInfoPopover.tsx | 3 + web/src/sections/input/InputChipStrip.tsx | 3 + web/src/sections/input/PasteTilePopover.tsx | 3 + .../knowledge/SourceHierarchyBrowser.tsx | 43 +++- .../PreviewModal/variants/docxVariant.tsx | 5 +- web/src/sections/sidebar/AppSidebar.tsx | 16 +- .../sections/sidebar/NotificationsPopover.tsx | 16 +- .../admin/GroupsPage/TokenLimitSection.tsx | 50 ++-- .../ImageGenerationContent.tsx | 16 +- 57 files changed, 1058 insertions(+), 624 deletions(-) diff --git a/web/.oxlintrc.json b/web/.oxlintrc.json index b605d03367e..764ed1b3fc9 100644 --- a/web/.oxlintrc.json +++ b/web/.oxlintrc.json @@ -1,6 +1,9 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["typescript", "unicorn", "oxc", "react", "nextjs"], + "jsPlugins": [ + { "name": "react-doctor", "specifier": "oxlint-plugin-react-doctor" } + ], "categories": { "correctness": "error" }, @@ -9,7 +12,15 @@ "react/exhaustive-deps": "off", "typescript/no-unused-vars": "off", "unicorn/no-thenable": "off", - "no-unused-vars": "off" + "no-unused-vars": "off", + "react-doctor/rules-of-hooks": "error", + "react-doctor/no-unguarded-browser-global-in-render-or-hook-init": "error", + "react-doctor/effect-needs-cleanup": "error", + "react-doctor/no-effect-with-fresh-deps": "error", + "react-doctor/aria-role": "error", + "react-doctor/no-ref-current-in-render": "error", + "react-doctor/no-impure-state-updater": "error", + "react-doctor/no-layout-property-animation": "error" }, "ignorePatterns": [ ".next", diff --git a/web/bun.lock b/web/bun.lock index 7a2226fe201..4b58b65e125 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -110,6 +110,7 @@ "jest-environment-jsdom": "^30.2.0", "oxfmt": "0.59.0", "oxlint": "^1.66.0", + "oxlint-plugin-react-doctor": "0.9.11", "stats.js": "^0.17.0", "storybook": "10.5.0", "tailwindcss": "^4.3.0", @@ -281,11 +282,11 @@ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], - "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], - "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], @@ -529,47 +530,47 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], - "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.127.0", "", { "os": "android", "cpu": "arm" }, "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.142.0", "", { "os": "android", "cpu": "arm" }, "sha512-ZiRGDutGsv1G6bL/ozy/koC0Sv39T1DqyoC4KD1DOy9ZoACm1O5UWhEK2c02Qdk+4lfLVkvFa/mQ0fm/4h1BtQ=="], - "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.127.0", "", { "os": "android", "cpu": "arm64" }, "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg=="], + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.142.0", "", { "os": "android", "cpu": "arm64" }, "sha512-WZkvGRLNQTz8lR9zP5nLjUdlroRCopBu3g9zF1p/laE6DzT1UbQo8Rdz5MWhaJUPYg/6gp+jo7HUgsyKaN1FtQ=="], - "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.127.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg=="], + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.142.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-l4khS8LQOOVYsGRVARo1gSaCT/aBSceUVXgtovWc2+drnxVuDr082WA3OCHVdVzIz5JIrP/y9CWsSKxBDNmYGg=="], - "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.127.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw=="], + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.142.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-QBsNF3nqlXmcH2B1YOPqQYmCJoy4HuIjUxGbBO/k5JAJUl68ghU2psRY2zPk+RyBaWqKP/qfL4oaFgEMCdwskA=="], - "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.127.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA=="], + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.142.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-b7Q7m4Cqc6XqNhri3R+QhU+GVy646Pn+bkdhrDdWym/Fdi0ZUa+d73H9dm5H91JtbtAQ/z1d8XKMW3oOV8a4tQ=="], - "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ=="], + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.142.0", "", { "os": "linux", "cpu": "arm" }, "sha512-3riVS5IhdH3uCZj1Y9ftDQlR0dvLsIlw/edrRqk8JhgNd5K0XSs+UBtgh50N13CAlW9/TXj6sVGXaKNBocd0Yg=="], - "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g=="], + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.142.0", "", { "os": "linux", "cpu": "arm" }, "sha512-NmXUOpgpTSkhl795TiXmWppTwmSJ92RC1qvD6e4XOF+slgmo3e6Ah+kEu+6AN8s7NAOEwqGmir58MgSQSWmBSA=="], - "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ=="], + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.142.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gc0EXsKtXgerujmU2Bql3u1L1HsSQ2774R83idq/FoNMPVV/RY/1ErFsvnit7KoiP/sLvzQixeUo4Ut0ic0wmw=="], - "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA=="], + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.142.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-F2XvmWSE0uWpie+jHKKIFgdVOe9ypGhkEZxKx5DuW215K6cbAC274yYaPkcM7EqY4Df3Weyhpcz3lsURyH2LVg=="], - "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.127.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ=="], + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.142.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-wLMbT21U/QxknQsk+VvNF0b9D2/aGWhcaQQQ+VYlE8FwD5+GoWZIPPXNzyHmkYyhm0KB3itL+TBavjMatqNnYA=="], - "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ=="], + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.142.0", "", { "os": "linux", "cpu": "none" }, "sha512-+G8F/4ckwT7FCJV4H2bt09xEzJbjNCfuL4Sp1AYNaFtFMVtgIGMuJlteT82U+K0UIZ/DzAR/LDlMFnEuajG7Kw=="], - "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g=="], + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.142.0", "", { "os": "linux", "cpu": "none" }, "sha512-hTsHtTLxMAfCo+rpF5K3qZJKW2NpPN/CHd4mYB3y7XlSdspHkd2gehDIofP64AacA9nWQw2tY3O7wR6UY8IVOA=="], - "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.127.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q=="], + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.142.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-6y7qYY3TCUDYjqswImdTGl92y+KA/80twALegQPN27kfY+bG7Ib1+L3jbmrCZQx6wrVnai9IPsEZp07I0hx7JQ=="], - "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ=="], + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.142.0", "", { "os": "linux", "cpu": "x64" }, "sha512-i69kAWU+2LgoH5bR+zWiiu+UzAw7Oxkwv7COeJTeY19pn4e70nKQcr9Pm6cL2Z0Z54d+gl9qADlK/0yyuCPiBA=="], - "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg=="], + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.142.0", "", { "os": "linux", "cpu": "x64" }, "sha512-4SQs678MmjYVrmhAgCWD4o0vpaFszXw9xLX5p2Z9MMFcltxiLkA88wQjh80YHjPrXtpyZ2CWI5m+1yNKM0m2Pw=="], - "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.127.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ=="], + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.142.0", "", { "os": "none", "cpu": "arm64" }, "sha512-YHpx9N7Ln3a++Tc8rv+H7mrK1zyJQOAwCFg8LZ3lTs1T5afGWeZrLPhPT9HLnIwSjCyJqPWVMIrMxbjcmBr2oQ=="], - "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.127.0", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ=="], + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.142.0", "", { "dependencies": { "@emnapi/core": "1.11.2", "@emnapi/runtime": "1.11.2", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-3pLDyY3+oogW73RM5uehNgAiR/Xfb7fvO2Q1Z1gIqZ2+50XDVQmBVlRkHXZTU4gKnQHpwETNsYQVsJ3joVB2iA=="], - "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.127.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw=="], + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.142.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Had/VeVY28Oyb0K+Q4FV8KCzoBycIh93oDK6pCbya9lkzdq+ikMHMgBubsdqqlybjJmQRawCQRrnBRHyQwYvcQ=="], - "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.127.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw=="], + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.142.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-GGi3+YphVHavvgs6gum2UXoNCqzHAmPt/nXkn8ZQZstV2Q1qZD1Mn8fz/nWrDkefHQtrG/+1/XrbMxsBTo6Svw=="], - "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="], + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.142.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Ny/Wv4Us1LGC/ljwNTp+Hx3r/pH15EFfeDF0p+n898gt+TtRd6C9SccHcuUhDiNTb8s5tt7jdeAMDRQZ4Vq6hg=="], - "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + "@oxc-project/types": ["@oxc-project/types@0.142.0", "", {}, "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ=="], "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="], @@ -893,6 +894,8 @@ "@sentry/webpack-plugin": ["@sentry/webpack-plugin@5.3.0", "", { "dependencies": { "@sentry/bundler-plugin-core": "5.3.0" }, "peerDependencies": { "webpack": ">=5.0.0" } }, "sha512-i3OQUrS0FZlXLgq57RIKDp+vHHzuvYKPCKewAPXULWKMsBXFGhP6veGRQ+6To/pmZkkXjEX5ofVNDy9C3jEPKQ=="], + "@shaderfrog/glsl-parser": ["@shaderfrog/glsl-parser@7.0.1", "", {}, "sha512-8mpfsoPeRhesY3pOrzNZBL8uG6N5GVX1EHLBYbd4gzKs+c7vaEIqpTNK5VrffU33qQN4cwpP2v3u4aPPBU32sw=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], @@ -1051,6 +1054,8 @@ "@types/doctrine": ["@types/doctrine@0.0.9", "", {}, "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA=="], + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -1117,6 +1122,8 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.67.0", "", {}, "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww=="], + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], @@ -1533,7 +1540,9 @@ "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], @@ -1543,7 +1552,7 @@ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], @@ -1849,29 +1858,29 @@ "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], @@ -2079,7 +2088,7 @@ "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "oxc-parser": ["oxc-parser@0.127.0", "", { "dependencies": { "@oxc-project/types": "^0.127.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.127.0", "@oxc-parser/binding-android-arm64": "0.127.0", "@oxc-parser/binding-darwin-arm64": "0.127.0", "@oxc-parser/binding-darwin-x64": "0.127.0", "@oxc-parser/binding-freebsd-x64": "0.127.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", "@oxc-parser/binding-linux-arm64-musl": "0.127.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-musl": "0.127.0", "@oxc-parser/binding-openharmony-arm64": "0.127.0", "@oxc-parser/binding-wasm32-wasi": "0.127.0", "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA=="], + "oxc-parser": ["oxc-parser@0.142.0", "", { "dependencies": { "@oxc-project/types": "^0.142.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.142.0", "@oxc-parser/binding-android-arm64": "0.142.0", "@oxc-parser/binding-darwin-arm64": "0.142.0", "@oxc-parser/binding-darwin-x64": "0.142.0", "@oxc-parser/binding-freebsd-x64": "0.142.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.142.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.142.0", "@oxc-parser/binding-linux-arm64-gnu": "0.142.0", "@oxc-parser/binding-linux-arm64-musl": "0.142.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.142.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.142.0", "@oxc-parser/binding-linux-riscv64-musl": "0.142.0", "@oxc-parser/binding-linux-s390x-gnu": "0.142.0", "@oxc-parser/binding-linux-x64-gnu": "0.142.0", "@oxc-parser/binding-linux-x64-musl": "0.142.0", "@oxc-parser/binding-openharmony-arm64": "0.142.0", "@oxc-parser/binding-wasm32-wasi": "0.142.0", "@oxc-parser/binding-win32-arm64-msvc": "0.142.0", "@oxc-parser/binding-win32-ia32-msvc": "0.142.0", "@oxc-parser/binding-win32-x64-msvc": "0.142.0" } }, "sha512-kKR+jPiRJYJDexVoziIg/FVGvr1fT1FZSSJOk6tVoMKKSlsf1Cso+cgGCJkOEDWOP174vRntCPFKg+AS7InWvw=="], "oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="], @@ -2087,6 +2096,8 @@ "oxlint": ["oxlint@1.71.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.71.0", "@oxlint/binding-android-arm64": "1.71.0", "@oxlint/binding-darwin-arm64": "1.71.0", "@oxlint/binding-darwin-x64": "1.71.0", "@oxlint/binding-freebsd-x64": "1.71.0", "@oxlint/binding-linux-arm-gnueabihf": "1.71.0", "@oxlint/binding-linux-arm-musleabihf": "1.71.0", "@oxlint/binding-linux-arm64-gnu": "1.71.0", "@oxlint/binding-linux-arm64-musl": "1.71.0", "@oxlint/binding-linux-ppc64-gnu": "1.71.0", "@oxlint/binding-linux-riscv64-gnu": "1.71.0", "@oxlint/binding-linux-riscv64-musl": "1.71.0", "@oxlint/binding-linux-s390x-gnu": "1.71.0", "@oxlint/binding-linux-x64-gnu": "1.71.0", "@oxlint/binding-linux-x64-musl": "1.71.0", "@oxlint/binding-openharmony-arm64": "1.71.0", "@oxlint/binding-win32-arm64-msvc": "1.71.0", "@oxlint/binding-win32-ia32-msvc": "1.71.0", "@oxlint/binding-win32-x64-msvc": "1.71.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-U1m1X+C0vDj7DC1e13IoZULzEcPczE7UOMTs8VlZGHUEIUaSTZKo5qkPsQEfzpgnQ29Pea/w3Xntk62UCecxZw=="], + "oxlint-plugin-react-doctor": ["oxlint-plugin-react-doctor@0.9.11", "", { "dependencies": { "@shaderfrog/glsl-parser": "^7.0.1", "@typescript-eslint/types": "^8.59.3", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "lightningcss": "^1.33.0", "oxc-parser": "^0.142.0" } }, "sha512-ZhW15wfFjQlUwAO6zG3jVZKse4/PBTCArhbibiidHmTlvOpPHPM1AjdYG13023pfsbbtVdy7Tb7KHfqbKt8rHg=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], @@ -2575,8 +2586,6 @@ "@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], - "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -2603,10 +2612,6 @@ "@onyx-ai/shared/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], - "@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], - - "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], "@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], @@ -2667,6 +2672,8 @@ "@sentry/cli/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + "@tailwindcss/node/tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], @@ -2705,10 +2712,6 @@ "cosmiconfig/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], - "esquery/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -2761,6 +2764,8 @@ "stacktrace-parser/type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], + "storybook/oxc-parser": ["oxc-parser@0.127.0", "", { "dependencies": { "@oxc-project/types": "^0.127.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.127.0", "@oxc-parser/binding-android-arm64": "0.127.0", "@oxc-parser/binding-darwin-arm64": "0.127.0", "@oxc-parser/binding-darwin-x64": "0.127.0", "@oxc-parser/binding-freebsd-x64": "0.127.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", "@oxc-parser/binding-linux-arm64-musl": "0.127.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-musl": "0.127.0", "@oxc-parser/binding-openharmony-arm64": "0.127.0", "@oxc-parser/binding-wasm32-wasi": "0.127.0", "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA=="], + "string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "style-dictionary/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -2785,6 +2790,8 @@ "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -2813,8 +2820,6 @@ "@jsonjoy.com/fs-snapshot/@jsonjoy.com/util/@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q=="], - "@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ=="], "@radix-ui/react-menubar/@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="], @@ -2835,6 +2840,30 @@ "@sentry/cli/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "@unrs/resolver-binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "jest-config/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], "jest-config/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], @@ -2901,6 +2930,48 @@ "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "storybook/oxc-parser/@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.127.0", "", { "os": "android", "cpu": "arm" }, "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ=="], + + "storybook/oxc-parser/@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.127.0", "", { "os": "android", "cpu": "arm64" }, "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg=="], + + "storybook/oxc-parser/@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.127.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg=="], + + "storybook/oxc-parser/@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.127.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw=="], + + "storybook/oxc-parser/@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.127.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA=="], + + "storybook/oxc-parser/@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ=="], + + "storybook/oxc-parser/@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g=="], + + "storybook/oxc-parser/@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ=="], + + "storybook/oxc-parser/@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA=="], + + "storybook/oxc-parser/@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.127.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ=="], + + "storybook/oxc-parser/@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ=="], + + "storybook/oxc-parser/@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g=="], + + "storybook/oxc-parser/@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.127.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q=="], + + "storybook/oxc-parser/@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ=="], + + "storybook/oxc-parser/@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg=="], + + "storybook/oxc-parser/@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.127.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ=="], + + "storybook/oxc-parser/@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.127.0", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ=="], + + "storybook/oxc-parser/@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.127.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw=="], + + "storybook/oxc-parser/@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.127.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw=="], + + "storybook/oxc-parser/@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="], + + "storybook/oxc-parser/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + "test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], "ts-unused-exports/tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], @@ -2957,6 +3028,8 @@ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "@jest/reporters/glob/minimatch/brace-expansion": ["brace-expansion@2.1.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A=="], @@ -2973,6 +3046,10 @@ "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "storybook/oxc-parser/@oxc-parser/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + + "storybook/oxc-parser/@oxc-parser/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], @@ -2984,5 +3061,7 @@ "jest-runtime/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "storybook/oxc-parser/@oxc-parser/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], } } diff --git a/web/lib/opal/src/components/inputs/input-select/components.tsx b/web/lib/opal/src/components/inputs/input-select/components.tsx index b65914baf90..5dbaecca3ac 100644 --- a/web/lib/opal/src/components/inputs/input-select/components.tsx +++ b/web/lib/opal/src/components/inputs/input-select/components.tsx @@ -355,16 +355,17 @@ function InputSelectItem({ // registrations. const childrenRef = React.useRef(children); const iconRef = React.useRef(icon); - childrenRef.current = children; - iconRef.current = icon; + React.useLayoutEffect(() => { + childrenRef.current = children; + iconRef.current = icon; + }, [children, icon]); // Layout effect so the trigger never paints the placeholder on first // render when a value is already selected. Radix mounts closed Content // into a detached fragment, so this runs even while the menu is closed. - // Keyed on the rendered content (plain-text key, since RichStr identity - // churns per render) so the trigger mirror re-renders when the selected - // option's label or icon changes without a value change. - const childrenKey = toPlainString(children); + // Keyed on the raw rendered content so RichStr formatting changes still + // refresh the trigger without reacting to identity churn. + const childrenKey = typeof children === "string" ? children : children.raw; React.useLayoutEffect(() => { if (!isSelected) return; setSelectedItemDisplay({ childrenRef, iconRef }); diff --git a/web/lib/opal/src/components/modal/components.tsx b/web/lib/opal/src/components/modal/components.tsx index 9b423278d9a..6095da86e9b 100644 --- a/web/lib/opal/src/components/modal/components.tsx +++ b/web/lib/opal/src/components/modal/components.tsx @@ -147,27 +147,19 @@ function ModalContent({ hasUserTypedRef.current = true; }, []); - const containerNodeRef = React.useRef(null); - - const contentRef = React.useCallback( - (node: HTMLDivElement | null) => { - if (containerNodeRef.current) { - containerNodeRef.current.removeEventListener( - "input", - handleInput, - true - ); - } - if (node) { - node.addEventListener("input", handleInput, true); - containerNodeRef.current = node; - } else { - containerNodeRef.current = null; - } - }, - [handleInput] + const [contentNode, setContentNode] = React.useState( + null ); + React.useEffect(() => { + if (!contentNode) return; + + contentNode.addEventListener("input", handleInput, true); + return () => { + contentNode.removeEventListener("input", handleInput, true); + }; + }, [contentNode, handleInput]); + const handleInteractOutside = React.useCallback( (e: Event) => { if (!preventAccidentalClose) { @@ -198,9 +190,9 @@ function ModalContent({ } else if (ref) { ref.current = node; } - contentRef(node); + setContentNode(node); }, - [ref, contentRef] + [ref] ); // Center on [data-main-container] when present (the content area beside diff --git a/web/lib/opal/src/components/table/hooks/useDataTable.ts b/web/lib/opal/src/components/table/hooks/useDataTable.ts index 7aecfc6cdd5..ab7507d475a 100644 --- a/web/lib/opal/src/components/table/hooks/useDataTable.ts +++ b/web/lib/opal/src/components/table/hooks/useDataTable.ts @@ -270,7 +270,9 @@ export default function useDataTable( // Single ref for the whole serverSide config — prevents effects from // re-firing when the consumer passes an inline object each render. const serverSideRef = useRef(serverSide); - serverSideRef.current = serverSide; + useEffect(() => { + serverSideRef.current = serverSide; + }, [serverSide]); useEffect(() => { if (!isServerSide) return; @@ -423,7 +425,9 @@ export default function useDataTable( // ---- selection change callback ------------------------------------------ const isFirstRenderRef = useRef(true); const onSelectionChangeRef = useRef(onSelectionChange); - onSelectionChangeRef.current = onSelectionChange; + useEffect(() => { + onSelectionChangeRef.current = onSelectionChange; + }, [onSelectionChange]); useEffect(() => { if (isFirstRenderRef.current) { diff --git a/web/lib/opal/src/components/tabs/hooks.ts b/web/lib/opal/src/components/tabs/hooks.ts index 4c65fe3a6bc..58c72227542 100644 --- a/web/lib/opal/src/components/tabs/hooks.ts +++ b/web/lib/opal/src/components/tabs/hooks.ts @@ -46,6 +46,9 @@ export function usePillIndicator( const [isScrolling, setIsScrolling] = useState(false); const scrollTimeoutRef = useRef(null); + // The scroll-debounce timer is ref-held and cleared in this effect's + // cleanup. The rule cannot trace handler-created timers. + // oxlint-disable-next-line react-doctor/effect-needs-cleanup useEffect(() => { if (!enabled) return; diff --git a/web/lib/opal/src/layouts/root/components.tsx b/web/lib/opal/src/layouts/root/components.tsx index db98e33630a..30181353e11 100644 --- a/web/lib/opal/src/layouts/root/components.tsx +++ b/web/lib/opal/src/layouts/root/components.tsx @@ -46,7 +46,9 @@ export function SidebarStateProvider({ const [folded, setFoldedInternal] = useState(defaultFolded); const onFoldedChangeRef = useRef(onFoldedChange); - onFoldedChangeRef.current = onFoldedChange; + useEffect(() => { + onFoldedChangeRef.current = onFoldedChange; + }, [onFoldedChange]); const setFolded: Dispatch> = useCallback((value) => { setFoldedInternal((prev) => diff --git a/web/package.json b/web/package.json index 347427942d7..51da7b27e88 100644 --- a/web/package.json +++ b/web/package.json @@ -137,6 +137,7 @@ "jest-environment-jsdom": "^30.2.0", "oxfmt": "0.59.0", "oxlint": "^1.66.0", + "oxlint-plugin-react-doctor": "0.9.11", "stats.js": "^0.17.0", "storybook": "10.5.0", "tailwindcss": "^4.3.0", diff --git a/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/StringPairListInput.tsx b/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/StringPairListInput.tsx index e3d6e4f348f..cd46fe83f07 100644 --- a/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/StringPairListInput.tsx +++ b/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/StringPairListInput.tsx @@ -46,14 +46,32 @@ const StringPairListInput: React.FC = ({ // Stable per-row keys so removing a middle row doesn't shift native input // state (focus/autofill) onto the row that takes its index. Index keys would; // content-derived keys would remount the row on every keystroke. New rows are - // seeded here; the remove handler splices so each id stays with its row. - const rowIdsRef = React.useRef([]); - const nextRowIdRef = React.useRef(0); - while (rowIdsRef.current.length < pairs.length) { - rowIdsRef.current.push(nextRowIdRef.current++); - } - if (rowIdsRef.current.length > pairs.length) { - rowIdsRef.current.length = pairs.length; + // seeded here, and the remove handler drops the key at that index so each id + // stays with its row. + const [rowKeys, setRowKeys] = React.useState<{ + keys: number[]; + nextKey: number; + }>({ + keys: pairs.map((_, index) => index), + nextKey: pairs.length, + }); + if (rowKeys.keys.length < pairs.length) { + const keysToAdd = pairs.length - rowKeys.keys.length; + setRowKeys({ + keys: [ + ...rowKeys.keys, + ...Array.from( + { length: keysToAdd }, + (_, index) => rowKeys.nextKey + index + ), + ], + nextKey: rowKeys.nextKey + keysToAdd, + }); + } else if (rowKeys.keys.length > pairs.length) { + setRowKeys({ + keys: rowKeys.keys.slice(0, pairs.length), + nextKey: rowKeys.nextKey, + }); } return ( @@ -89,7 +107,7 @@ const StringPairListInput: React.FC = ({ {pairs.map((_, index) => (
= ({ type="button" tooltip="Remove" onClick={() => { - rowIdsRef.current.splice(index, 1); + setRowKeys((prev) => ({ + keys: prev.keys.filter((_, i) => i !== index), + nextKey: prev.nextKey, + })); arrayHelpers.remove(index); }} /> @@ -136,9 +157,9 @@ const StringPairListInput: React.FC = ({ icon={SvgPlusCircle} prominence="secondary" type="button" - onClick={() => - arrayHelpers.push({ [leftKey]: "", [rightKey]: "" }) - } + onClick={() => { + arrayHelpers.push({ [leftKey]: "", [rightKey]: "" }); + }} > Add New diff --git a/web/src/app/app/message/HumanMessage.tsx b/web/src/app/app/message/HumanMessage.tsx index 90aa49e8804..272bf735e72 100644 --- a/web/src/app/app/message/HumanMessage.tsx +++ b/web/src/app/app/message/HumanMessage.tsx @@ -56,8 +56,6 @@ function MessageEditing({ className={cn( "w-full h-full resize-none outline-hidden bg-transparent overflow-y-scroll whitespace-normal break-word" )} - aria-multiline - role="textarea" value={editedContent} style={{ scrollbarWidth: "thin" }} onChange={(e) => { diff --git a/web/src/app/app/message/MultiModelResponseView.tsx b/web/src/app/app/message/MultiModelResponseView.tsx index fe430a4bd5a..0979b3ea085 100644 --- a/web/src/app/app/message/MultiModelResponseView.tsx +++ b/web/src/app/app/message/MultiModelResponseView.tsx @@ -102,28 +102,20 @@ export default function MultiModelResponseView({ const [selectionExiting, setSelectionExiting] = useState(false); // Measures the overflow-hidden carousel container for responsive preferred-panel sizing. const [trackContainerW, setTrackContainerW] = useState(0); - const roRef = useRef(null); const trackContainerElRef = useRef(null); + const [trackContainerEl, setTrackContainerEl] = + useState(null); const trackContainerRef = useCallback((el: HTMLDivElement | null) => { trackContainerElRef.current = el; - if (roRef.current) { - roRef.current.disconnect(); - roRef.current = null; - } - if (!el) return; - const ro = new ResizeObserver(([entry]) => { - setTrackContainerW(entry?.contentRect.width ?? 0); - }); - ro.observe(el); - setTrackContainerW(el.offsetWidth); - roRef.current = ro; + setTrackContainerEl(el); }, []); // Measures the preferred panel's height to cap non-preferred panels in selection mode. const [preferredPanelHeight, setPreferredPanelHeight] = useState< number | null >(null); - const preferredRoRef = useRef(null); + const [preferredPanelEl, setPreferredPanelEl] = + useState(null); // Refs to each panel wrapper for height animation on deselect const panelElsRef = useRef>(new Map()); @@ -148,21 +140,36 @@ export default function MultiModelResponseView({ }); }, [preferredPanelHeight, preferredIndex, hiddenPanels, responses]); - const preferredPanelRef = useCallback((el: HTMLDivElement | null) => { - if (preferredRoRef.current) { - preferredRoRef.current.disconnect(); - preferredRoRef.current = null; - } - if (!el) { + useLayoutEffect(() => { + if (!trackContainerEl) return; + const ro = new ResizeObserver(([entry]) => { + setTrackContainerW(entry?.contentRect.width ?? 0); + }); + ro.observe(trackContainerEl); + setTrackContainerW(trackContainerEl.offsetWidth); + return () => ro.disconnect(); + }, [trackContainerEl]); + + useLayoutEffect(() => { + if (!preferredPanelEl) { setPreferredPanelHeight(null); return; } const ro = new ResizeObserver(([entry]) => { setPreferredPanelHeight(entry?.contentRect.height ?? 0); }); - ro.observe(el); - setPreferredPanelHeight(el.offsetHeight); - preferredRoRef.current = ro; + ro.observe(preferredPanelEl); + setPreferredPanelHeight(preferredPanelEl.offsetHeight); + return () => ro.disconnect(); + }, [preferredPanelEl]); + + useEffect(() => { + return () => { + if (deselectTimeoutRef.current !== null) { + clearTimeout(deselectTimeoutRef.current); + deselectTimeoutRef.current = null; + } + }; }, []); const isGenerating = useMemo( @@ -554,7 +561,7 @@ export default function MultiModelResponseView({ } else { panelElsRef.current.delete(r.modelIndex); } - if (isPref) preferredPanelRef(el); + if (isPref) setPreferredPanelEl(el); }} style={{ width: `${selectionEntered ? finalW : startW}px`, diff --git a/web/src/app/app/message/messageComponents/hooks/useAuthErrors.ts b/web/src/app/app/message/messageComponents/hooks/useAuthErrors.ts index ff8a4a89ccf..29a67406b70 100644 --- a/web/src/app/app/message/messageComponents/hooks/useAuthErrors.ts +++ b/web/src/app/app/message/messageComponents/hooks/useAuthErrors.ts @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import { useMemo } from "react"; import { CustomToolDelta, Packet, @@ -11,43 +11,37 @@ interface AuthError { } export function useAuthErrors(rawPackets: Packet[]): AuthError[] { - const stateRef = useRef<{ processedCount: number; errors: AuthError[] }>({ - processedCount: 0, - errors: [], - }); - - // Reset if packets shrunk (e.g. new message) - if (rawPackets.length < stateRef.current.processedCount) { - stateRef.current = { processedCount: 0, errors: [] }; - } + // Keyed on the packet array so re-renders between packet batches reuse + // the same result identity instead of rescanning. + return useMemo(() => computeAuthErrors(rawPackets), [rawPackets]); +} + +function computeAuthErrors(rawPackets: Packet[]): AuthError[] { + const errors: AuthError[] = []; + + for (const packet of rawPackets) { + if (packet.obj.type !== PacketType.CUSTOM_TOOL_DELTA) { + continue; + } + + const delta = packet.obj as CustomToolDelta; + if (!delta.error?.is_auth_error) { + continue; + } + + const alreadyPresent = errors.some( + (error) => + (delta.tool_id != null && error.toolId === delta.tool_id) || + (delta.tool_id == null && error.toolName === delta.tool_name) + ); - // Process only new packets (incremental, like usePacketProcessor) - if (rawPackets.length > stateRef.current.processedCount) { - let newErrors = stateRef.current.errors; - for (let i = stateRef.current.processedCount; i < rawPackets.length; i++) { - const packet = rawPackets[i]!; - if (packet.obj.type === PacketType.CUSTOM_TOOL_DELTA) { - const delta = packet.obj as CustomToolDelta; - if (delta.error?.is_auth_error) { - const alreadyPresent = newErrors.some( - (e) => - (delta.tool_id != null && e.toolId === delta.tool_id) || - (delta.tool_id == null && e.toolName === delta.tool_name) - ); - if (!alreadyPresent) { - newErrors = [ - ...newErrors, - { toolName: delta.tool_name, toolId: delta.tool_id ?? null }, - ]; - } - } - } + if (!alreadyPresent) { + errors.push({ + toolName: delta.tool_name, + toolId: delta.tool_id ?? null, + }); } - stateRef.current = { - processedCount: rawPackets.length, - errors: newErrors, - }; } - return stateRef.current.errors; + return errors; } diff --git a/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx b/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx index ac99ec60f17..a425d0fa4a0 100644 --- a/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx +++ b/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx @@ -339,8 +339,10 @@ export const MessageTextRenderer: MessageRenderer< // never change — otherwise every typewriter tick would invalidate // React reconciliation on the markdown subtree. const stateRef = useRef(state); + // oxlint-disable-next-line react-doctor/no-ref-current-in-render -- render-phase mirror keeps markdownComponents identities stable (see block comment above) stateRef.current = state; const processedContentRef = useRef(processedContent); + // oxlint-disable-next-line react-doctor/no-ref-current-in-render -- render-phase mirror keeps markdownComponents identities stable (see block comment above) processedContentRef.current = processedContent; const markdownComponents = useMemo( diff --git a/web/src/app/app/message/messageComponents/timeline/hooks/usePacedTurnGroups.ts b/web/src/app/app/message/messageComponents/timeline/hooks/usePacedTurnGroups.ts index 49d57b85711..a31d0bfe915 100644 --- a/web/src/app/app/message/messageComponents/timeline/hooks/usePacedTurnGroups.ts +++ b/web/src/app/app/message/messageComponents/timeline/hooks/usePacedTurnGroups.ts @@ -59,7 +59,7 @@ interface PacingState { nodeId: string | null; } -function createInitialPacingState(): PacingState { +function createInitialPacingState(nodeId: string): PacingState { return { revealedStepKeys: new Set(), lastRevealedPacketType: null, @@ -67,7 +67,7 @@ function createInitialPacingState(): PacingState { pacingTimer: null, toolPacingComplete: false, stopPacketSeen: false, - nodeId: null, + nodeId, }; } @@ -97,8 +97,11 @@ export function usePacedTurnGroups( pacedDisplayGroups: GroupedPacket[]; pacedFinalAnswerComing: boolean; } { + // Stable nodeId string for comparison + const nodeIdStr = String(nodeId); + // Ref-based pacing state (no re-renders) - const stateRef = useRef(createInitialPacingState()); + const stateRef = useRef(createInitialPacingState(nodeIdStr)); // Track previous finalAnswerComing to detect tool-after-message transitions const prevFinalAnswerComingRef = useRef(finalAnswerComing); @@ -110,21 +113,13 @@ export function usePacedTurnGroups( // Trigger re-render when content should update // Used in useMemo dependencies since state.revealedStepKeys is stored in a ref const [revealTrigger, setRevealTrigger] = useState(0); - - // Stable nodeId string for comparison - const nodeIdStr = String(nodeId); - - // Reset on nodeId change - if (stateRef.current.nodeId !== nodeIdStr) { - if (stateRef.current.pacingTimer) { - clearTimeout(stateRef.current.pacingTimer); - } - stateRef.current = createInitialPacingState(); - stateRef.current.nodeId = nodeIdStr; - prevPacedRef.current = []; - } - - const state = stateRef.current; + const [timerTrigger, setTimerTrigger] = useState(0); + const resetState = useMemo( + () => createInitialPacingState(nodeIdStr), + [nodeIdStr] + ); + const hasNodeChanged = stateRef.current.nodeId !== nodeIdStr; + const state = hasNodeChanged ? resetState : stateRef.current; // Bypass pacing for completed messages (old messages loaded from history) // If stopPacketSeen is true on first render, return everything immediately @@ -145,7 +140,7 @@ export function usePacedTurnGroups( // Schedule next step if more pending (always delay, regardless of type) if (state.pendingSteps.length > 0) { - state.pacingTimer = setTimeout(revealNextPendingStep, PACING_DELAY_MS); + setTimerTrigger((t) => t + 1); setRevealTrigger((t) => t + 1); return; } @@ -157,6 +152,48 @@ export function usePacedTurnGroups( setRevealTrigger((t) => t + 1); }, []); + useEffect(() => { + if (!hasNodeChanged) { + return; + } + + if (stateRef.current.pacingTimer) { + clearTimeout(stateRef.current.pacingTimer); + } + + stateRef.current = resetState; + prevPacedRef.current = []; + prevFinalAnswerComingRef.current = finalAnswerComing; + }, [finalAnswerComing, hasNodeChanged, resetState]); + + useEffect(() => { + const state = stateRef.current; + + if ( + shouldBypassPacing || + state.pendingSteps.length === 0 || + state.pacingTimer + ) { + return; + } + + const timer = setTimeout(() => { + if (stateRef.current.pacingTimer === timer) { + stateRef.current.pacingTimer = null; + } + revealNextPendingStep(); + }, PACING_DELAY_MS); + + state.pacingTimer = timer; + + return () => { + clearTimeout(timer); + if (stateRef.current.pacingTimer === timer) { + stateRef.current.pacingTimer = null; + } + }; + }, [revealNextPendingStep, shouldBypassPacing, timerTrigger]); + // Process incoming turn groups useEffect(() => { // Skip processing when bypassing pacing @@ -256,7 +293,7 @@ export function usePacedTurnGroups( // Start timer if not already running if (!state.pacingTimer && state.pendingSteps.length === 1) { - state.pacingTimer = setTimeout(revealNextPendingStep, PACING_DELAY_MS); + setTimerTrigger((t) => t + 1); } } @@ -270,17 +307,11 @@ export function usePacedTurnGroups( finalAnswerComing, revealNextPendingStep, shouldBypassPacing, + // Re-process after the node-change reset so the fresh state's first step + // reveals immediately even when the group identities are unchanged. + nodeIdStr, ]); - // Cleanup timer on unmount - useEffect(() => { - return () => { - if (stateRef.current.pacingTimer) { - clearTimeout(stateRef.current.pacingTimer); - } - }; - }, []); - // Build paced turn groups from revealed step keys // Memoized to prevent unnecessary re-renders in downstream components // revealTrigger is included because state.revealedStepKeys is stored in a ref @@ -305,7 +336,7 @@ export function usePacedTurnGroups( // Stabilize: reuse previous TurnGroup objects when their content hasn't changed. // This preserves referential equality for completed groups, preventing // unnecessary re-renders in downstream components (e.g. SearchChipList). - const prev = prevPacedRef.current; + const prev = hasNodeChanged ? [] : prevPacedRef.current; if (prev.length === result.length) { let allMatch = true; for (let i = 0; i < result.length; i++) { @@ -332,10 +363,13 @@ export function usePacedTurnGroups( } } - prevPacedRef.current = result; return result; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [toolTurnGroups, revealTrigger, shouldBypassPacing]); + }, [toolTurnGroups, revealTrigger, shouldBypassPacing, hasNodeChanged]); + + useEffect(() => { + prevPacedRef.current = pacedTurnGroups; + }, [pacedTurnGroups]); // Only return display groups when tool pacing is complete (or bypassing). // Also bypass when stop packet is already seen (e.g. history reload of stopped messages) diff --git a/web/src/app/app/message/messageComponents/timeline/hooks/usePacketProcessor.ts b/web/src/app/app/message/messageComponents/timeline/hooks/usePacketProcessor.ts index ba7f099f693..27338acde64 100644 --- a/web/src/app/app/message/messageComponents/timeline/hooks/usePacketProcessor.ts +++ b/web/src/app/app/message/messageComponents/timeline/hooks/usePacketProcessor.ts @@ -1,3 +1,6 @@ +/* oxlint-disable react-doctor/no-ref-current-in-render -- render-phase + incremental processing is the core design: the packet cursor makes + replays idempotent and consumers need the state in the same commit. */ import { useRef, useState, useMemo, useCallback } from "react"; import { Packet, diff --git a/web/src/app/app/message/messageComponents/timeline/renderers/reasoning/ReasoningRenderer.tsx b/web/src/app/app/message/messageComponents/timeline/renderers/reasoning/ReasoningRenderer.tsx index 4d6825797fc..f4a0d68be28 100644 --- a/web/src/app/app/message/messageComponents/timeline/renderers/reasoning/ReasoningRenderer.tsx +++ b/web/src/app/app/message/messageComponents/timeline/renderers/reasoning/ReasoningRenderer.tsx @@ -111,7 +111,6 @@ export const ReasoningRenderer: MessageRenderer< const [reasoningStartTime, setReasoningStartTime] = useState( null ); - const timeoutRef = useRef(null); const completionHandledRef = useRef(false); // Track when reasoning starts @@ -124,35 +123,32 @@ export const ReasoningRenderer: MessageRenderer< // Handle reasoning completion with minimum duration useEffect(() => { if ( - hasEnd && - reasoningStartTime !== null && - !completionHandledRef.current + !hasEnd || + reasoningStartTime === null || + completionHandledRef.current ) { - completionHandledRef.current = true; - const elapsedTime = Date.now() - reasoningStartTime; - const minimumThinkingDuration = animate ? THINKING_MIN_DURATION_MS : 0; + return; + } - if (elapsedTime >= minimumThinkingDuration) { - // Enough time has passed, complete immediately + const complete = () => { + if (!completionHandledRef.current) { + completionHandledRef.current = true; onComplete(); - } else { - // Not enough time has passed, delay completion - const remainingTime = minimumThinkingDuration - elapsedTime; - timeoutRef.current = setTimeout(() => { - onComplete(); - }, remainingTime); } + }; + + const elapsedTime = Date.now() - reasoningStartTime; + const minimumThinkingDuration = animate ? THINKING_MIN_DURATION_MS : 0; + + if (elapsedTime >= minimumThinkingDuration) { + complete(); + return; } - }, [hasEnd, reasoningStartTime, animate, onComplete]); - // Cleanup timeout on unmount - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); + const remainingTime = minimumThinkingDuration - elapsedTime; + const timeout = setTimeout(complete, remainingTime); + return () => clearTimeout(timeout); + }, [hasEnd, reasoningStartTime, animate, onComplete]); // Markdown renderer callback for ExpandableTextDisplay // Uses collapsed components (no spacing) in collapsed view, normal spacing in expanded modal diff --git a/web/src/app/craft/components/BuildMessageList.tsx b/web/src/app/craft/components/BuildMessageList.tsx index d82581f970f..ccc74b1ba9d 100644 --- a/web/src/app/craft/components/BuildMessageList.tsx +++ b/web/src/app/craft/components/BuildMessageList.tsx @@ -213,7 +213,9 @@ export default function BuildMessageList({ initial={ opts.isCurrentStream ? { opacity: 0, y: -4, height: 0 } : false } + // oxlint-disable-next-line react-doctor/no-layout-property-animation -- height 0/auto must reflow the message list, transform cannot animate={{ opacity: 1, y: 0, height: "auto" }} + // oxlint-disable-next-line react-doctor/no-layout-property-animation -- height/marginTop collapse must reflow the message list, transform cannot exit={{ opacity: 0, y: -6, height: 0, marginTop: 0 }} transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }} > diff --git a/web/src/app/craft/components/ChatPanel.tsx b/web/src/app/craft/components/ChatPanel.tsx index 8ea17c37674..155d70f2145 100644 --- a/web/src/app/craft/components/ChatPanel.tsx +++ b/web/src/app/craft/components/ChatPanel.tsx @@ -206,6 +206,9 @@ export default function BuildChatPanel({ turnId: string; timer: ReturnType; } | null>(null); + const nameSessionTimeoutRef = useRef | null>( + null + ); const isPreProvisioning = useIsPreProvisioning(); const isPreProvisioningFailed = useIsPreProvisioningFailed(); const preProvisionedSessionId = usePreProvisionedSessionId(); @@ -536,7 +539,16 @@ export default function BuildChatPanel({ // Schedule naming after delay (message will be saved by then) // Note: Don't call refreshSessionHistory() here - it would overwrite the // optimistic update from consumePreProvisionedSession() before the message is saved - setTimeout(() => nameBuildSession(newSessionId), 1000); + if (nameSessionTimeoutRef.current !== null) { + clearTimeout(nameSessionTimeoutRef.current); + } + // Session naming is a store action that must survive unmount. Firing + // before the 1s save window would name against an unsaved message. + // oxlint-disable-next-line react-doctor/effect-needs-cleanup + nameSessionTimeoutRef.current = setTimeout(() => { + nameSessionTimeoutRef.current = null; + nameBuildSession(newSessionId); + }, 1000); // Stream the response (uses session ID directly, not currentSessionId) await streamMessage(newSessionId, message, chosen, attachments); diff --git a/web/src/app/craft/components/OpencodeDebugLogs.tsx b/web/src/app/craft/components/OpencodeDebugLogs.tsx index f5b954560e4..1c717ac259d 100644 --- a/web/src/app/craft/components/OpencodeDebugLogs.tsx +++ b/web/src/app/craft/components/OpencodeDebugLogs.tsx @@ -115,7 +115,9 @@ function LogStreamPane({ open }: LogStreamPaneProps) { // Refs mirror the state inside the SSE reader's hot loop without // forcing it to re-bind on every state change. const followRef = useRef(follow); - followRef.current = follow; + useEffect(() => { + followRef.current = follow; + }, [follow]); const appendLine = useCallback((text: string) => { const line: LogLine = { diff --git a/web/src/app/craft/contexts/BuildContext.tsx b/web/src/app/craft/contexts/BuildContext.tsx index 6e05fdfc0ce..eb9bbfa53f1 100644 --- a/web/src/app/craft/contexts/BuildContext.tsx +++ b/web/src/app/craft/contexts/BuildContext.tsx @@ -42,12 +42,10 @@ export function BuildProvider({ children }: BuildProviderProps) { }, []); const toggleVideoBackground = useCallback(() => { - setVideoBackgroundEnabled((prev) => { - const next = !prev; - localStorage.setItem(VIDEO_BACKGROUND_STORAGE_KEY, String(next)); - return next; - }); - }, []); + const next = !videoBackgroundEnabled; + setVideoBackgroundEnabled(next); + localStorage.setItem(VIDEO_BACKGROUND_STORAGE_KEY, String(next)); + }, [videoBackgroundEnabled]); const value = useMemo( () => ({ diff --git a/web/src/app/craft/contexts/UploadFilesContext.tsx b/web/src/app/craft/contexts/UploadFilesContext.tsx index 79672acb015..192a317b962 100644 --- a/web/src/app/craft/contexts/UploadFilesContext.tsx +++ b/web/src/app/craft/contexts/UploadFilesContext.tsx @@ -270,6 +270,7 @@ export function UploadFilesProvider({ children }: UploadFilesProviderProps) { const [currentMessageFiles, setCurrentMessageFiles] = useState( [] ); + const currentMessageFilesRef = useRef([]); const [activeSessionId, setActiveSessionId] = useState(null); // Get triggerFilesRefresh from the store to refresh the file explorer @@ -305,6 +306,10 @@ export function UploadFilesProvider({ children }: UploadFilesProviderProps) { ); }, [currentMessageFiles]); + useEffect(() => { + currentMessageFilesRef.current = currentMessageFiles; + }, [currentMessageFiles]); + // ========================================================================= // Internal operations (not exposed to consumers) // ========================================================================= @@ -318,26 +323,23 @@ export function UploadFilesProvider({ children }: UploadFilesProviderProps) { async (sessionId: string): Promise => { if (isUploadingPendingRef.current) return; - // Read current files and find pending ones atomically - let pendingFiles: BuildFile[] = []; - setCurrentMessageFiles((prev) => { - pendingFiles = prev.filter( - (f) => f.status === UploadFileStatus.PENDING && f.file - ); - // Mark as uploading in the same state update to avoid race conditions - if (pendingFiles.length > 0) { - return prev.map((f) => - pendingFiles.some((pf) => pf.id === f.id) - ? { ...f, status: UploadFileStatus.UPLOADING } - : f - ); - } - return prev; - }); + const currentFiles = currentMessageFilesRef.current; + const pendingFiles = currentFiles.filter( + (f) => f.status === UploadFileStatus.PENDING && f.file + ); if (pendingFiles.length === 0) return; isUploadingPendingRef.current = true; + // Functional update so a concurrent add/remove in the same batch is + // never clobbered. The ref syncs from state in its own effect. + setCurrentMessageFiles((prev) => + prev.map((f) => + pendingFiles.some((pf) => pf.id === f.id) + ? { ...f, status: UploadFileStatus.UPLOADING } + : f + ) + ); try { // Upload in parallel @@ -699,23 +701,22 @@ export function UploadFilesProvider({ children }: UploadFilesProviderProps) { // Track this deletion to prevent refetch race condition activeDeletionsRef.current.add(fileId); - // Use functional update to get current state and avoid stale closures - let removedFile: BuildFile | null = null; - let removedIndex = -1; - - setCurrentMessageFiles((prev) => { - const index = prev.findIndex((f) => f.id === fileId); - if (index === -1) return prev; + const currentFiles = currentMessageFilesRef.current; + const removedIndex = currentFiles.findIndex((f) => f.id === fileId); + if (removedIndex === -1) { + activeDeletionsRef.current.delete(fileId); + return; + } - // Capture file info for potential rollback and backend deletion - const file = prev[index]; - if (!file) return prev; - removedFile = file; - removedIndex = index; + const removedFile = currentFiles[removedIndex]; + if (!removedFile) { + activeDeletionsRef.current.delete(fileId); + return; + } - // Return filtered array (optimistic removal) - return prev.filter((f) => f.id !== fileId); - }); + // Functional update keeps concurrent same-batch changes. The ref syncs + // from state in its own effect. + setCurrentMessageFiles((prev) => prev.filter((f) => f.id !== fileId)); // After state update, trigger backend deletion if needed // Use setTimeout to ensure state update has completed diff --git a/web/src/app/craft/v1/tasks/components/ScheduleTaskForm.tsx b/web/src/app/craft/v1/tasks/components/ScheduleTaskForm.tsx index 0e6c0d17301..abe5fb28bf6 100644 --- a/web/src/app/craft/v1/tasks/components/ScheduleTaskForm.tsx +++ b/web/src/app/craft/v1/tasks/components/ScheduleTaskForm.tsx @@ -146,27 +146,27 @@ export default function ScheduleTaskForm({ router.push(connectionPath); return; } - setSkillPicker((prev) => { - if (!prev.open) return prev; - const replacement = `${pickerEntryPromptPrefix(entry)} `; - const newPrompt = - prompt.slice(0, prev.slashIndex) + - replacement + - prompt.slice(prev.slashIndex + 1 + prev.query.length); - setPrompt(newPrompt); + if (!skillPicker.open) return; - const cursorPos = prev.slashIndex + replacement.length; - const textarea = promptTextareaRef.current; - if (textarea) { - requestAnimationFrame(() => { - textarea.focus(); - textarea.setSelectionRange(cursorPos, cursorPos); - }); - } - return { ...prev, open: false }; - }); + const replacement = `${pickerEntryPromptPrefix(entry)} `; + const newPrompt = + prompt.slice(0, skillPicker.slashIndex) + + replacement + + prompt.slice(skillPicker.slashIndex + 1 + skillPicker.query.length); + setPrompt(newPrompt); + + const cursorPos = skillPicker.slashIndex + replacement.length; + const textarea = promptTextareaRef.current; + if (textarea) { + requestAnimationFrame(() => { + textarea.focus(); + textarea.setSelectionRange(cursorPos, cursorPos); + }); + } + + setSkillPicker((prev) => (prev.open ? { ...prev, open: false } : prev)); }, - [prompt, router] + [prompt, router, skillPicker] ); const compiled = compileLocalPayloadToUtcCron(mode, payload); diff --git a/web/src/app/ee/admin/export-logs/page.tsx b/web/src/app/ee/admin/export-logs/page.tsx index d7a84caad63..32990c7e0ed 100644 --- a/web/src/app/ee/admin/export-logs/page.tsx +++ b/web/src/app/ee/admin/export-logs/page.tsx @@ -97,6 +97,11 @@ export default function ExportLogsPage() { const [isStarting, setIsStarting] = useState(false); const [isDownloading, setIsDownloading] = useState(false); const downloadedExportIdRef = useRef(null); + // Pending deferred revocation: cancelling the timer must also revoke the URL. + const pendingRevokeRef = useRef<{ + timer: ReturnType; + url: string; + } | null>(null); const { data: status, error: statusError } = useSWR( exportId === null ? null : SWR_KEYS.logExportStatus(exportId), @@ -142,6 +147,16 @@ export default function ExportLogsPage() { setExportId(null); }, [statusError]); + useEffect(() => { + return () => { + if (pendingRevokeRef.current !== null) { + clearTimeout(pendingRevokeRef.current.timer); + URL.revokeObjectURL(pendingRevokeRef.current.url); + pendingRevokeRef.current = null; + } + }; + }, []); + const downloadBundle = useCallback(async (id: string): Promise => { // Mark before any await: an attempt is in flight or succeeded, and only // failure re-arms the auto-download below. Owning this here keeps every @@ -160,7 +175,18 @@ export default function ExportLogsPage() { downloadFile(extractFilename(response), { url }); // Deferred like downloadFile's content mode: the click's download // dereferences the blob URL asynchronously. - setTimeout(() => URL.revokeObjectURL(url), 0); + if (pendingRevokeRef.current !== null) { + clearTimeout(pendingRevokeRef.current.timer); + URL.revokeObjectURL(pendingRevokeRef.current.url); + } + // Released on unmount and on replacement. The rule cannot trace the + // handle through the pendingRevokeRef object. + // oxlint-disable-next-line react-doctor/effect-needs-cleanup + const timer = setTimeout(() => { + URL.revokeObjectURL(url); + pendingRevokeRef.current = null; + }, 0); + pendingRevokeRef.current = { url, timer }; } catch (error) { console.error("Error downloading log export:", error); toast.error("Failed to download the log export."); diff --git a/web/src/app/ee/admin/standard-answer/page.tsx b/web/src/app/ee/admin/standard-answer/page.tsx index 5171587b606..4c3b490a844 100644 --- a/web/src/app/ee/admin/standard-answer/page.tsx +++ b/web/src/app/ee/admin/standard-answer/page.tsx @@ -240,8 +240,6 @@ const StandardAnswersTable = ({