From 1235b226d38dd453fd7c15719642688bf07283bf Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 16 Apr 2026 13:35:29 -0700 Subject: [PATCH 1/4] improvement(ui): remove React anti-patterns, fix CSP violations --- .../commands/you-might-not-need-a-callback.md | 33 ++- .../message-actions/message-actions.tsx | 44 ++-- .../resource-header/resource-header.tsx | 8 +- .../resource-tabs/resource-tabs.tsx | 206 ++++++++++++------ .../mothership-view/mothership-view.tsx | 16 +- .../user-input/components/drop-overlay.tsx | 4 +- .../app/workspace/[workspaceId]/logs/logs.tsx | 4 +- apps/sim/lib/core/security/csp.ts | 5 + 8 files changed, 205 insertions(+), 115 deletions(-) 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 @@ -221,10 +334,7 @@ export function ResourceTabs({ anchorIdRef.current = null } - const existingKeys = useMemo( - () => new Set(resources.map((r) => `${r.type}:${r.id}`)), - [resources] - ) + const existingKeys = new Set(resources.map((r) => `${r.type}:${r.id}`)) const handleAdd = useCallback( (resource: MothershipResource) => { @@ -476,7 +586,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 +603,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..f6dee8e5122 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -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/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': [ From 54e4c6ff5b35cd926ee564cebf855a49c6464be0 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 16 Apr 2026 13:45:09 -0700 Subject: [PATCH 2/4] =?UTF-8?q?fix(ui):=20restore=20useMemo=20on=20existin?= =?UTF-8?q?gKeys=20=E2=80=94=20it=20is=20observed=20by=20useAvailableResou?= =?UTF-8?q?rces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/resource-tabs/resource-tabs.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx index 58811e66adb..2fbd230a5f5 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx @@ -334,7 +334,10 @@ export function ResourceTabs({ anchorIdRef.current = null } - const existingKeys = new Set(resources.map((r) => `${r.type}:${r.id}`)) + const existingKeys = useMemo( + () => new Set(resources.map((r) => `${r.type}:${r.id}`)), + [resources] + ) const handleAdd = useCallback( (resource: MothershipResource) => { From dcfc5f677a06aa2fd18497eb0b875a59ecc10485 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 16 Apr 2026 13:48:05 -0700 Subject: [PATCH 3/4] improvement(ui): add RefreshCw icon, update Bell SVG, active state styling for header actions --- .../resource-header/resource-header.tsx | 1 + .../app/workspace/[workspaceId]/logs/logs.tsx | 10 +++--- apps/sim/components/emcn/icons/bell.tsx | 10 +++--- apps/sim/components/emcn/icons/index.ts | 1 + apps/sim/components/emcn/icons/refresh-cw.tsx | 31 +++++++++++++++++++ 5 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 apps/sim/components/emcn/icons/refresh-cw.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx index 6b30586dd6f..68baec3f2b6 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx @@ -106,6 +106,7 @@ export const ResourceHeader = memo(function ResourceHeader({ variant='subtle' className={cn( 'px-2 py-1 text-caption', + action.active !== undefined && 'rounded-lg', action.active === true && 'bg-[var(--surface-active)] hover-hover:bg-[var(--surface-active)]', action.active === false && 'hover-hover:bg-[var(--surface-hover)]' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index f6dee8e5122..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, 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 ( + + ) +} From 49d9af2663119d03e29ee3f777763e1bee64beca Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 16 Apr 2026 13:58:01 -0700 Subject: [PATCH 4/4] minor UI improvements --- .../workspace/[workspaceId]/components/inline-rename-input.tsx | 3 ++- .../components/resource-options-bar/resource-options-bar.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/inline-rename-input.tsx b/apps/sim/app/workspace/[workspaceId]/components/inline-rename-input.tsx index 99cae0891b2..f35aa7d33bf 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/inline-rename-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/inline-rename-input.tsx @@ -25,6 +25,7 @@ export function InlineRenameInput({ value, onChange, onSubmit, onCancel }: Inlin ref={inputRef} type='text' value={value} + size={Math.max(value.length + 2, 5)} onChange={(e) => onChange(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') onSubmit() @@ -32,7 +33,7 @@ export function InlineRenameInput({ value, onChange, onSubmit, onCancel }: Inlin }} onBlur={onSubmit} onClick={(e) => e.stopPropagation()} - className='min-w-0 flex-1 truncate border-0 bg-transparent p-0 font-medium text-[var(--text-body)] text-sm outline-none focus:outline-none focus:ring-0' + className='min-w-0 border-0 bg-transparent p-0 font-medium text-[var(--text-body)] text-sm outline-none focus:outline-none focus:ring-0' /> ) } diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx index d8e09d813c7..91e8eaaee4e 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx @@ -222,7 +222,7 @@ const SortDropdown = memo(function SortDropdown({ config }: { config: SortConfig Sort - + {options.map((option) => { const isActive = active?.column === option.id const Icon = option.icon