From 84051e2f2bd88cc814f0111c0189cb2489b151b1 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 1 Jul 2026 12:37:29 -0700 Subject: [PATCH 1/2] improvement(chat): code-split resource preview panel out of initial /chat bundle Lazy-load the MothershipView resource-preview panel (file-viewer, rich-markdown, CSV/PDF stack) via React.lazy + local Suspense so it is not in the initial /chat bundle; it only renders once a chat has messages. Remove its now-dead barrel re-export so the split takes effect (this app has no sideEffects:false, so a leftover barrel edge drags the heavy module back into the initial chunk). Also a small behavior-preserving perf pass on the input surface: - toSorted (non-mutating) instead of spread+sort in use-skill-auto-mention - Map first-match lookup instead of .find()-in-loop in prompt-editor Document the code-split/barrel gotcha in .claude/rules/sim-imports.md. --- .claude/rules/sim-imports.md | 14 +++++++ .../[workspaceId]/home/components/index.ts | 1 - .../prompt-editor/prompt-editor.tsx | 7 +++- .../hooks/use-skill-auto-mention.ts | 2 +- .../app/workspace/[workspaceId]/home/home.tsx | 40 +++++++++++++------ 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/.claude/rules/sim-imports.md b/.claude/rules/sim-imports.md index 74bf556db4d..83a939ca16d 100644 --- a/.claude/rules/sim-imports.md +++ b/.claude/rules/sim-imports.md @@ -31,6 +31,20 @@ import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/component import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard/dashboard' ``` +## Code-splitting through barrels + +When you `lazy(() => import(...))` a component to keep it out of a route's initial bundle, import the **deep module path** (`./components/foo/foo`), never the barrel — and **delete the now-dead barrel re-export** of that component. This app has no `"sideEffects": false` in `apps/sim/package.json`, so webpack keeps a barrel's re-export edge to the heavy module whenever any sibling still imports that barrel. A leftover `export { Foo } from './foo'` line therefore drags `Foo` (and its transitive deps) back into the initial chunk and silently defeats the split. Verify the split with a production bundle diff, not just by eyeballing the `lazy()` call. + +```typescript +// ✓ Good — deep lazy import + no barrel edge left behind +const MothershipView = lazy(() => + import('./components/mothership-view/mothership-view').then((m) => ({ default: m.MothershipView })) +) +// (and remove `export { MothershipView } from './mothership-view'` from components/index.ts) +``` + +Wrap the lazy component in a **local ``** so its suspension resolves at the nearest boundary instead of bubbling to the page-level fallback (which would flash the whole route). `React.lazy(memo(forwardRef(...)))` forwards a DOM `ref` correctly in React 19 — but during the fallback window `ref.current` is `null`, so every consumer must guard it (`if (!el) return` / `el?.`). + ## No Re-exports Do not re-export from non-barrel files. Import directly from the source. diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/index.ts index 799c9f2c2fd..8425c127a6e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/index.ts @@ -11,7 +11,6 @@ export { MothershipResourcesProvider, useMothershipResources, } from './mothership-resources-context' -export { MothershipView } from './mothership-view' export { QueuedMessages } from './queued-messages' export { SuggestedActions } from './suggested-actions' export { UserInput, type UserInputHandle } from './user-input' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.tsx index d716c1d32ba..b3d9cb78edd 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.tsx @@ -122,6 +122,11 @@ export function PromptEditor({ return {displayText} } + const contextByLabel = new Map() + for (const c of contexts) { + if (!contextByLabel.has(c.label)) contextByLabel.set(c.label, c) + } + const elements: React.ReactNode[] = [] let lastIndex = 0 for (let i = 0; i < ranges.length; i++) { @@ -133,7 +138,7 @@ export function PromptEditor({ } const mentionLabel = stripMentionTrigger(range.token) - const matchingCtx = contexts.find((c) => c.label === mentionLabel) + const matchingCtx = contextByLabel.get(mentionLabel) const mentionIconNode = matchingCtx ? ( b - a)) { + for (const idx of slashIndices.toSorted((a, b) => b - a)) { result = result.slice(0, idx) + SKILL_CHIP_TRIGGER + result.slice(idx + 1) } return result diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 6e75925b9a9..c206ffd9bf0 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -2,7 +2,9 @@ import { type Dispatch, + lazy, type SetStateAction, + Suspense, useCallback, useEffect, useMemo, @@ -49,7 +51,6 @@ import { CreditsChip, MothershipChat, MothershipResourcesProvider, - MothershipView, SuggestedActions, UserInput, type UserInputHandle, @@ -59,6 +60,17 @@ import type { FileAttachmentForApi, MothershipResource, MothershipResourceType } const logger = createLogger('Home') +/** + * The resource preview panel pulls in the file-viewer stack (rich-markdown + * editor, CSV/PDF viewers). It only renders once a chat has messages, so it is + * code-split out of the initial `/chat` bundle and loaded on demand. + */ +const MothershipView = lazy(() => + import('./components/mothership-view/mothership-view').then((m) => ({ + default: m.MothershipView, + })) +) + interface HomeProps { chatId?: string userName?: string @@ -491,18 +503,20 @@ export function Home({ chatId, userName, userId }: HomeProps) { reorderResources={reorderResources} collapseResource={collapseResource} > - + + + {isResourceCollapsed && ( From 5c0f78604573ac698bc8582bc9b7ea6efaddee7c Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 1 Jul 2026 13:06:22 -0700 Subject: [PATCH 2/2] fix(chat): drop unsafe toSorted, keep code-split + Map lookup toSorted (ES2023) is not polyfilled by SWC and crashes iOS15/Safari<16 in client bundles, so revert use-skill-auto-mention to the non-mutating [...arr].sort() it had. The code-split of MothershipView and the first-match Map lookup in prompt-editor are unaffected. Tighten the sim-imports code-split note to state webpack *can* retain the barrel edge (removing the dead re-export is the guaranteed fix). --- .claude/rules/sim-imports.md | 2 +- .../home/components/user-input/hooks/use-skill-auto-mention.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/rules/sim-imports.md b/.claude/rules/sim-imports.md index 83a939ca16d..0449d96e11e 100644 --- a/.claude/rules/sim-imports.md +++ b/.claude/rules/sim-imports.md @@ -33,7 +33,7 @@ import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboa ## Code-splitting through barrels -When you `lazy(() => import(...))` a component to keep it out of a route's initial bundle, import the **deep module path** (`./components/foo/foo`), never the barrel — and **delete the now-dead barrel re-export** of that component. This app has no `"sideEffects": false` in `apps/sim/package.json`, so webpack keeps a barrel's re-export edge to the heavy module whenever any sibling still imports that barrel. A leftover `export { Foo } from './foo'` line therefore drags `Foo` (and its transitive deps) back into the initial chunk and silently defeats the split. Verify the split with a production bundle diff, not just by eyeballing the `lazy()` call. +When you `lazy(() => import(...))` a component to keep it out of a route's initial bundle, import the **deep module path** (`./components/foo/foo`), never the barrel — and **delete the now-dead barrel re-export** of that component. This app has no `"sideEffects": false` in `apps/sim/package.json`, so when any sibling still imports that barrel, webpack can conservatively keep the barrel's re-export edge to the heavy module. A leftover `export { Foo } from './foo'` line can therefore drag `Foo` (and its transitive deps) back into the initial chunk and silently defeat the split. Removing the dead re-export is the guaranteed fix; verify with a production bundle diff, not by eyeballing the `lazy()` call. ```typescript // ✓ Good — deep lazy import + no barrel edge left behind diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/hooks/use-skill-auto-mention.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/hooks/use-skill-auto-mention.ts index e9896fe490a..8a962686df2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/hooks/use-skill-auto-mention.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/hooks/use-skill-auto-mention.ts @@ -176,7 +176,7 @@ export function useSkillAutoMention({ skills, setSelectedContexts }: UseSkillAut // descending so earlier replacements don't shift later indices; the // sentinel is one code unit wide like '/'. let result = text - for (const idx of slashIndices.toSorted((a, b) => b - a)) { + for (const idx of [...slashIndices].sort((a, b) => b - a)) { result = result.slice(0, idx) + SKILL_CHIP_TRIGGER + result.slice(idx + 1) } return result