diff --git a/web/components/sf/chat-mode.tsx b/web/components/sf/chat-mode.tsx index acef448b8..2c14868a2 100644 --- a/web/components/sf/chat-mode.tsx +++ b/web/components/sf/chat-mode.tsx @@ -2364,6 +2364,7 @@ function ToolExecutionBlock({ tool }: { tool: CompletedToolExecution }) { const hasVisibleResult = Boolean(diff || resultText.trim() || isError); if (!hasVisibleResult) return; autoExpandedRef.current = true; + // eslint-disable-next-line react-hooks/set-state-in-effect -- ref-guarded, runs at most once setExpanded(true); }, [diff, resultText, isError]); @@ -2652,6 +2653,7 @@ export function ChatPane({ className, onOpenAction }: ChatPaneProps) { role: "assistant", content: seg.content, complete: true, + // eslint-disable-next-line react-hooks/purity -- display timestamp for in-progress message timestamp: Date.now(), }, }); @@ -2820,6 +2822,7 @@ export function ChatPane({ className, onOpenAction }: ChatPaneProps) { role: "assistant", content: item.content, complete: false, + // eslint-disable-next-line react-hooks/purity -- display timestamp for streaming message timestamp: Date.now(), }} isThinking={item.isThinking} diff --git a/web/components/sf/file-content-viewer.tsx b/web/components/sf/file-content-viewer.tsx index 4b1782365..000b54ae0 100644 --- a/web/components/sf/file-content-viewer.tsx +++ b/web/components/sf/file-content-viewer.tsx @@ -760,6 +760,7 @@ export function FileContentViewer({ // Reset edit content when the source content changes (e.g. after save + re-fetch) useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect -- syncing local copy when prop changes setEditContent(content); }, [content]); diff --git a/web/components/sf/files-view.tsx b/web/components/sf/files-view.tsx index fe23df60f..29ca1dd1b 100644 --- a/web/components/sf/files-view.tsx +++ b/web/components/sf/files-view.tsx @@ -712,12 +712,14 @@ export function FilesView() { // Fetch tree when tab changes and data isn't cached useEffect(() => { if (!treeLoaded) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await fetchTree(activeRoot); } }, [activeRoot, treeLoaded, fetchTree]); // Initial load useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await fetchTree("sf"); }, [fetchTree]); @@ -852,6 +854,7 @@ export function FilesView() { consumedPendingRef.current = true; const { root, path } = pendingFileRequest; pendingFileRequest = null; + // eslint-disable-next-line react-hooks/set-state-in-effect -- async, setState runs after await void processFileOpen(root, path); } }, [processFileOpen]); @@ -1283,6 +1286,7 @@ export function FilesView() { ); if (hasStateMd) { autoSelectedRef.current = true; + // eslint-disable-next-line react-hooks/set-state-in-effect -- async, setState runs after await void openFileTab("sf", "STATE.md"); } }, [sfTree, openTabs.length, openFileTab]); diff --git a/web/components/sf/onboarding/step-dev-root.tsx b/web/components/sf/onboarding/step-dev-root.tsx index baebdc2ea..c8f2b2d87 100644 --- a/web/components/sf/onboarding/step-dev-root.tsx +++ b/web/components/sf/onboarding/step-dev-root.tsx @@ -72,6 +72,7 @@ function InlineFolderBrowser({ }, []); useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await void browse(); }, [browse]); diff --git a/web/components/sf/onboarding/step-remote.tsx b/web/components/sf/onboarding/step-remote.tsx index 8ff52a93f..d1ed4bec8 100644 --- a/web/components/sf/onboarding/step-remote.tsx +++ b/web/components/sf/onboarding/step-remote.tsx @@ -288,7 +288,7 @@ export function StepRemote({ onBack, onNext }: StepRemoteProps) { {channelId.trim().length > 0 && !CHANNEL_ID_PATTERNS[channel].test(channelId.trim()) && (
- Doesn't match the expected format for {channel} + Doesn't match the expected format for {channel}
)} diff --git a/web/components/sf/onboarding/step-welcome.tsx b/web/components/sf/onboarding/step-welcome.tsx index 289964dd5..ba84dd40a 100644 --- a/web/components/sf/onboarding/step-welcome.tsx +++ b/web/components/sf/onboarding/step-welcome.tsx @@ -46,7 +46,7 @@ export function StepWelcome({ onNext }: StepWelcomeProps) { transition={{ delay: 0.16, duration: 0.4 }} className="max-w-sm text-[15px] leading-relaxed text-muted-foreground" > - Let's get your workspace ready. This takes about a minute. + Let's get your workspace ready. This takes about a minute. {/* Steps preview */} diff --git a/web/components/sf/projects-view.tsx b/web/components/sf/projects-view.tsx index c9df438e1..2471ec5a8 100644 --- a/web/components/sf/projects-view.tsx +++ b/web/components/sf/projects-view.tsx @@ -400,7 +400,6 @@ export function ProjectsPanel({ setNewProjectOpen(false); handleSelectProject(newProject); }, - // eslint-disable-next-line react-hooks/exhaustive-deps [handleSelectProject], ); @@ -638,6 +637,7 @@ function NewProjectDialog({ useEffect(() => { if (open) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- reset form state on dialog open setName(""); setError(null); setCreating(false); @@ -809,6 +809,7 @@ function FolderPickerDialog({ useEffect(() => { if (open) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await void browse(initialPath ?? undefined); } }, [open, initialPath, browse]); @@ -1439,7 +1440,7 @@ export function ProjectSelectionGate() { {/* Empty filter state */} {filteredProjects.length === 0 && filter.trim() && (- Doesn't match the expected format for{" "} + Doesn't match the expected format for{" "} {selectedChannelOption.label}
)} @@ -1416,11 +1418,12 @@ export function ExperimentalPanel() { if (!data && !busy && state.phase === "idle") { refresh(); } - }, [state.phase, refresh, data, busy]); // eslint-disable-line react-hooks/exhaustive-deps + }, [state.phase, refresh, data, busy]); // Sync local state from loaded prefs useEffect(() => { if (!prefs) return; + // eslint-disable-next-line react-hooks/set-state-in-effect -- syncing local copy when prefs load setFlags({ rtk: prefs.experimental?.rtk === true }); }, [prefs]); diff --git a/web/components/sf/update-banner.tsx b/web/components/sf/update-banner.tsx index c3a0e184d..e76025d73 100644 --- a/web/components/sf/update-banner.tsx +++ b/web/components/sf/update-banner.tsx @@ -34,6 +34,7 @@ export function UpdateBanner() { // Initial fetch on mount useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await void fetchStatus(); }, [fetchStatus]); diff --git a/web/components/sf/visualizer-view.tsx b/web/components/sf/visualizer-view.tsx index 1d4c74f5b..d88fcbbc8 100644 --- a/web/components/sf/visualizer-view.tsx +++ b/web/components/sf/visualizer-view.tsx @@ -1374,6 +1374,7 @@ export function VisualizerView() { }, [projectCwd]); useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await fetchData(); const interval = setInterval(fetchData, 10_000); return () => clearInterval(interval); diff --git a/web/components/ui/carousel.tsx b/web/components/ui/carousel.tsx index 1b116b27b..7d35e28ea 100644 --- a/web/components/ui/carousel.tsx +++ b/web/components/ui/carousel.tsx @@ -94,6 +94,7 @@ function Carousel({ React.useEffect(() => { if (!api) return; + // eslint-disable-next-line react-hooks/set-state-in-effect -- carousel API callback, setState via onSelect onSelect(api); api.on("reInit", onSelect); api.on("select", onSelect); diff --git a/web/components/ui/use-mobile.tsx b/web/components/ui/use-mobile.tsx index 0a892310f..31f6bf365 100644 --- a/web/components/ui/use-mobile.tsx +++ b/web/components/ui/use-mobile.tsx @@ -13,6 +13,7 @@ export function useIsMobile() { setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); }; mql.addEventListener("change", onChange); + // eslint-disable-next-line react-hooks/set-state-in-effect -- initial sync with viewport, runs once on mount setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); return () => mql.removeEventListener("change", onChange); }, []); diff --git a/web/hooks/use-mobile.ts b/web/hooks/use-mobile.ts index 0a892310f..31f6bf365 100644 --- a/web/hooks/use-mobile.ts +++ b/web/hooks/use-mobile.ts @@ -13,6 +13,7 @@ export function useIsMobile() { setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); }; mql.addEventListener("change", onChange); + // eslint-disable-next-line react-hooks/set-state-in-effect -- initial sync with viewport, runs once on mount setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); return () => mql.removeEventListener("change", onChange); }, []);