diff --git a/.claude/commands/you-might-not-need-a-callback.md b/.claude/commands/you-might-not-need-a-callback.md index d2251024004..c25d4c32eab 100644 --- a/.claude/commands/you-might-not-need-a-callback.md +++ b/.claude/commands/you-might-not-need-a-callback.md @@ -16,17 +16,34 @@ User arguments: $ARGUMENTS Read before analyzing: 1. https://react.dev/reference/react/useCallback — official docs on when useCallback is actually needed +## The one rule that matters + +`useCallback` is only useful when **something observes the reference**. Ask: does anything care if this function gets a new identity on re-render? + +Observers that care about reference stability: +- A `useEffect` that lists the function in its deps array +- A `useMemo` that lists the function in its deps array +- Another `useCallback` that lists the function in its deps array +- A child component wrapped in `React.memo` that receives the function as a prop + +If none of those apply — if the function is only called inline, or passed to a non-memoized child, or assigned to a native element event — the reference is unobserved and `useCallback` adds overhead with zero benefit. + ## Anti-patterns to detect -1. **useCallback on functions not passed as props or deps**: No benefit if only called within the same component. -2. **useCallback with deps that change every render**: Memoization is wasted. -3. **useCallback on handlers passed to native elements**: ` + + +

{displayName}

+
+ + {showGapAfter && ( +
+ )} +
+ ) +}) + interface ResourceTabsProps { workspaceId: string chatId?: string @@ -476,7 +589,6 @@ export function ResourceTabs({ onDrop={handleDrop} > {resources.map((resource, idx) => { - const config = getResourceConfig(resource.type) const displayName = nameLookup.get(`${resource.type}:${resource.id}`) ?? resource.title const isActive = activeId === resource.id const isHovered = hoveredTabId === resource.id @@ -494,73 +606,26 @@ export function ResourceTabs({ draggedIdx !== idx return ( -
- {showGapBefore && ( -
- )} - - - - - -

{displayName}

-
-
- {showGapAfter && ( -
- )} -
+ ) })}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx index ccdebb09b68..fcfb08ff948 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx @@ -1,6 +1,6 @@ 'use client' -import { forwardRef, memo, useCallback, useEffect, useState } from 'react' +import { forwardRef, memo, useState } from 'react' import type { FilePreviewSession } from '@/lib/copilot/request/session' import { cn } from '@/lib/core/utils/cn' import { getFileExtension } from '@/lib/uploads/utils/file-utils' @@ -80,15 +80,13 @@ export const MothershipView = memo( : undefined const [previewMode, setPreviewMode] = useState('preview') - const [prevActiveId, setPrevActiveId] = useState(active?.id) - const handleCyclePreview = useCallback(() => setPreviewMode((m) => PREVIEW_CYCLE[m]), []) + const handleCyclePreview = () => setPreviewMode((m) => PREVIEW_CYCLE[m]) - useEffect(() => { - if (active?.id !== prevActiveId) { - setPrevActiveId(active?.id) - setPreviewMode('preview') - } - }, [active?.id, prevActiveId]) + const [prevActiveId, setPrevActiveId] = useState(active?.id) + if (prevActiveId !== active?.id) { + setPrevActiveId(active?.id) + setPreviewMode('preview') + } const isActivePreviewable = canEdit && diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/drop-overlay.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/drop-overlay.tsx index e786c2d1892..1a5fa7fadbc 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/drop-overlay.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/drop-overlay.tsx @@ -1,6 +1,6 @@ 'use client' -import React from 'react' +import { memo } from 'react' import { AudioIcon, CsvIcon, @@ -25,7 +25,7 @@ const DROP_OVERLAY_ICONS = [ VideoIcon, ] as const -export const DropOverlay = React.memo(function DropOverlay() { +export const DropOverlay = memo(function DropOverlay() { return (
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 8a69912ade5..90c24c5ee59 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -14,7 +14,7 @@ import { DropdownMenuContent, DropdownMenuTrigger, Library, - Loader, + RefreshCw, } from '@/components/emcn' import { DatePicker } from '@/components/emcn/components/date-picker/date-picker' import { dollarsToCredits } from '@/lib/billing/credits/conversion' @@ -1086,9 +1086,9 @@ export default function Logs() { ) const refreshIcon = useMemo(() => { - if (!isVisuallyRefreshing) return Loader - const Spinning = (props: React.SVGProps) => - Spinning.displayName = 'SpinningLoader' + if (!isVisuallyRefreshing) return RefreshCw + const Spinning = (props: React.SVGProps) => + Spinning.displayName = 'SpinningRefresh' return Spinning }, [isVisuallyRefreshing]) @@ -1106,7 +1106,7 @@ export default function Logs() { onClick: handleOpenNotificationSettings, }, { - label: '', + label: 'Refresh', icon: refreshIcon, onClick: handleRefresh, disabled: isVisuallyRefreshing, @@ -1114,12 +1114,12 @@ export default function Logs() { { label: 'Logs', onClick: () => setViewMode('logs'), - disabled: !isDashboardView, + active: !isDashboardView, }, { label: 'Dashboard', onClick: () => setViewMode('dashboard'), - disabled: isDashboardView, + active: isDashboardView, }, ], [ diff --git a/apps/sim/components/emcn/icons/bell.tsx b/apps/sim/components/emcn/icons/bell.tsx index ebbf72ad849..58b82d8babf 100644 --- a/apps/sim/components/emcn/icons/bell.tsx +++ b/apps/sim/components/emcn/icons/bell.tsx @@ -7,20 +7,20 @@ import type { SVGProps } from 'react' export function Bell(props: SVGProps) { return ( ) } diff --git a/apps/sim/components/emcn/icons/index.ts b/apps/sim/components/emcn/icons/index.ts index 272e2cb897d..5baf3dd57e2 100644 --- a/apps/sim/components/emcn/icons/index.ts +++ b/apps/sim/components/emcn/icons/index.ts @@ -60,6 +60,7 @@ export { PillsRing } from './pills-ring' export { Play, PlayOutline } from './play' export { Plus } from './plus' export { Redo } from './redo' +export { RefreshCw } from './refresh-cw' export { Rocket } from './rocket' export { Rows3 } from './rows3' export { Search } from './search' diff --git a/apps/sim/components/emcn/icons/refresh-cw.tsx b/apps/sim/components/emcn/icons/refresh-cw.tsx new file mode 100644 index 00000000000..97fc8173f8c --- /dev/null +++ b/apps/sim/components/emcn/icons/refresh-cw.tsx @@ -0,0 +1,31 @@ +import type { SVGProps } from 'react' +import styles from '@/components/emcn/icons/animate/loader.module.css' +import { cn } from '@/lib/core/utils/cn' + +export interface RefreshCwProps extends SVGProps { + animate?: boolean +} + +export function RefreshCw({ animate = false, className, ...props }: RefreshCwProps) { + return ( + + ) +} diff --git a/apps/sim/lib/core/security/csp.ts b/apps/sim/lib/core/security/csp.ts index d2905f0e301..9420878e6d5 100644 --- a/apps/sim/lib/core/security/csp.ts +++ b/apps/sim/lib/core/security/csp.ts @@ -22,6 +22,7 @@ export interface CSPDirectives { 'media-src'?: string[] 'font-src'?: string[] 'connect-src'?: string[] + 'worker-src'?: string[] 'frame-src'?: string[] 'frame-ancestors'?: string[] 'form-action'?: string[] @@ -83,6 +84,8 @@ const STATIC_CONNECT_SRC = [ 'https://api.github.com', 'https://github.com/*', 'https://challenges.cloudflare.com', + ...(isReactGrabEnabled ? ['https://www.react-grab.com'] : []), + ...(isDev ? ['ws://localhost:4722'] : []), ...(isHosted ? [ 'https://www.googletagmanager.com', @@ -90,6 +93,7 @@ const STATIC_CONNECT_SRC = [ 'https://*.analytics.google.com', 'https://analytics.google.com', 'https://www.google.com', + 'https://analytics.ahrefs.com', ] : []), ] as const @@ -146,6 +150,7 @@ export const buildTimeCSPDirectives: CSPDirectives = { ], 'media-src': ["'self'", 'blob:'], + 'worker-src': ["'self'", 'blob:'], 'font-src': ["'self'", 'https://fonts.gstatic.com'], 'connect-src': [