fix(web): resolve ESLint regressions from eslint-config-next upgrade

- Escape unescaped entities (react/no-unescaped-entities) in step-remote,
  step-welcome, projects-view, settings-panels
- Add targeted eslint-disable-next-line for react-hooks/set-state-in-effect
  on established async-fetch and prop-sync patterns in useEffect bodies:
  chat-mode, file-content-viewer, files-view, step-dev-root, projects-view,
  settings-panels, update-banner, visualizer-view, carousel, use-mobile
- Add targeted eslint-disable-next-line for react-hooks/purity on Date.now()
  display timestamps in streaming chat messages (chat-mode)
- Remove now-unused eslint-disable directives (projects-view, settings-panels)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-10 12:18:58 +02:00
parent b0a8f32a10
commit 2d34d3a386
13 changed files with 24 additions and 6 deletions

View file

@ -2364,6 +2364,7 @@ function ToolExecutionBlock({ tool }: { tool: CompletedToolExecution }) {
const hasVisibleResult = Boolean(diff || resultText.trim() || isError); const hasVisibleResult = Boolean(diff || resultText.trim() || isError);
if (!hasVisibleResult) return; if (!hasVisibleResult) return;
autoExpandedRef.current = true; autoExpandedRef.current = true;
// eslint-disable-next-line react-hooks/set-state-in-effect -- ref-guarded, runs at most once
setExpanded(true); setExpanded(true);
}, [diff, resultText, isError]); }, [diff, resultText, isError]);
@ -2652,6 +2653,7 @@ export function ChatPane({ className, onOpenAction }: ChatPaneProps) {
role: "assistant", role: "assistant",
content: seg.content, content: seg.content,
complete: true, complete: true,
// eslint-disable-next-line react-hooks/purity -- display timestamp for in-progress message
timestamp: Date.now(), timestamp: Date.now(),
}, },
}); });
@ -2820,6 +2822,7 @@ export function ChatPane({ className, onOpenAction }: ChatPaneProps) {
role: "assistant", role: "assistant",
content: item.content, content: item.content,
complete: false, complete: false,
// eslint-disable-next-line react-hooks/purity -- display timestamp for streaming message
timestamp: Date.now(), timestamp: Date.now(),
}} }}
isThinking={item.isThinking} isThinking={item.isThinking}

View file

@ -760,6 +760,7 @@ export function FileContentViewer({
// Reset edit content when the source content changes (e.g. after save + re-fetch) // Reset edit content when the source content changes (e.g. after save + re-fetch)
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- syncing local copy when prop changes
setEditContent(content); setEditContent(content);
}, [content]); }, [content]);

View file

@ -712,12 +712,14 @@ export function FilesView() {
// Fetch tree when tab changes and data isn't cached // Fetch tree when tab changes and data isn't cached
useEffect(() => { useEffect(() => {
if (!treeLoaded) { if (!treeLoaded) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await
fetchTree(activeRoot); fetchTree(activeRoot);
} }
}, [activeRoot, treeLoaded, fetchTree]); }, [activeRoot, treeLoaded, fetchTree]);
// Initial load // Initial load
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await
fetchTree("sf"); fetchTree("sf");
}, [fetchTree]); }, [fetchTree]);
@ -852,6 +854,7 @@ export function FilesView() {
consumedPendingRef.current = true; consumedPendingRef.current = true;
const { root, path } = pendingFileRequest; const { root, path } = pendingFileRequest;
pendingFileRequest = null; pendingFileRequest = null;
// eslint-disable-next-line react-hooks/set-state-in-effect -- async, setState runs after await
void processFileOpen(root, path); void processFileOpen(root, path);
} }
}, [processFileOpen]); }, [processFileOpen]);
@ -1283,6 +1286,7 @@ export function FilesView() {
); );
if (hasStateMd) { if (hasStateMd) {
autoSelectedRef.current = true; autoSelectedRef.current = true;
// eslint-disable-next-line react-hooks/set-state-in-effect -- async, setState runs after await
void openFileTab("sf", "STATE.md"); void openFileTab("sf", "STATE.md");
} }
}, [sfTree, openTabs.length, openFileTab]); }, [sfTree, openTabs.length, openFileTab]);

View file

@ -72,6 +72,7 @@ function InlineFolderBrowser({
}, []); }, []);
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await
void browse(); void browse();
}, [browse]); }, [browse]);

View file

@ -288,7 +288,7 @@ export function StepRemote({ onBack, onNext }: StepRemoteProps) {
{channelId.trim().length > 0 && {channelId.trim().length > 0 &&
!CHANNEL_ID_PATTERNS[channel].test(channelId.trim()) && ( !CHANNEL_ID_PATTERNS[channel].test(channelId.trim()) && (
<p className="text-xs text-destructive/70"> <p className="text-xs text-destructive/70">
Doesn't match the expected format for {channel} Doesn&apos;t match the expected format for {channel}
</p> </p>
)} )}
</div> </div>

View file

@ -46,7 +46,7 @@ export function StepWelcome({ onNext }: StepWelcomeProps) {
transition={{ delay: 0.16, duration: 0.4 }} transition={{ delay: 0.16, duration: 0.4 }}
className="max-w-sm text-[15px] leading-relaxed text-muted-foreground" className="max-w-sm text-[15px] leading-relaxed text-muted-foreground"
> >
Let's get your workspace ready. This takes about a minute. Let&apos;s get your workspace ready. This takes about a minute.
</motion.p> </motion.p>
{/* Steps preview */} {/* Steps preview */}

View file

@ -400,7 +400,6 @@ export function ProjectsPanel({
setNewProjectOpen(false); setNewProjectOpen(false);
handleSelectProject(newProject); handleSelectProject(newProject);
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps
[handleSelectProject], [handleSelectProject],
); );
@ -638,6 +637,7 @@ function NewProjectDialog({
useEffect(() => { useEffect(() => {
if (open) { if (open) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- reset form state on dialog open
setName(""); setName("");
setError(null); setError(null);
setCreating(false); setCreating(false);
@ -809,6 +809,7 @@ function FolderPickerDialog({
useEffect(() => { useEffect(() => {
if (open) { if (open) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await
void browse(initialPath ?? undefined); void browse(initialPath ?? undefined);
} }
}, [open, initialPath, browse]); }, [open, initialPath, browse]);
@ -1439,7 +1440,7 @@ export function ProjectSelectionGate() {
{/* Empty filter state */} {/* Empty filter state */}
{filteredProjects.length === 0 && filter.trim() && ( {filteredProjects.length === 0 && filter.trim() && (
<div className="px-4 py-8 text-center text-xs text-muted-foreground"> <div className="px-4 py-8 text-center text-xs text-muted-foreground">
No projects matching "{filter}" No projects matching &quot;{filter}&quot;
</div> </div>
)} )}
</div> </div>

View file

@ -830,11 +830,13 @@ export function RemoteQuestionsPanel() {
}, []); }, []);
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await
void fetchApiStatus(); void fetchApiStatus();
}, [fetchApiStatus]); }, [fetchApiStatus]);
useEffect(() => { useEffect(() => {
if (existingConfig?.channel) { if (existingConfig?.channel) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- syncing local state from loaded config
setChannel(existingConfig.channel); setChannel(existingConfig.channel);
setChannelId(existingConfig.channelId ?? ""); setChannelId(existingConfig.channelId ?? "");
setTimeoutMinutes(existingConfig.timeoutMinutes ?? 5); setTimeoutMinutes(existingConfig.timeoutMinutes ?? 5);
@ -1103,7 +1105,7 @@ export function RemoteQuestionsPanel() {
{channelId.trim().length > 0 && {channelId.trim().length > 0 &&
!CHANNEL_ID_PATTERNS[channel].test(channelId.trim()) && ( !CHANNEL_ID_PATTERNS[channel].test(channelId.trim()) && (
<p className="text-[11px] text-destructive/70"> <p className="text-[11px] text-destructive/70">
Doesn't match the expected format for{" "} Doesn&apos;t match the expected format for{" "}
{selectedChannelOption.label} {selectedChannelOption.label}
</p> </p>
)} )}
@ -1416,11 +1418,12 @@ export function ExperimentalPanel() {
if (!data && !busy && state.phase === "idle") { if (!data && !busy && state.phase === "idle") {
refresh(); refresh();
} }
}, [state.phase, refresh, data, busy]); // eslint-disable-line react-hooks/exhaustive-deps }, [state.phase, refresh, data, busy]);
// Sync local state from loaded prefs // Sync local state from loaded prefs
useEffect(() => { useEffect(() => {
if (!prefs) return; 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 }); setFlags({ rtk: prefs.experimental?.rtk === true });
}, [prefs]); }, [prefs]);

View file

@ -34,6 +34,7 @@ export function UpdateBanner() {
// Initial fetch on mount // Initial fetch on mount
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await
void fetchStatus(); void fetchStatus();
}, [fetchStatus]); }, [fetchStatus]);

View file

@ -1374,6 +1374,7 @@ export function VisualizerView() {
}, [projectCwd]); }, [projectCwd]);
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch, setState runs after await
fetchData(); fetchData();
const interval = setInterval(fetchData, 10_000); const interval = setInterval(fetchData, 10_000);
return () => clearInterval(interval); return () => clearInterval(interval);

View file

@ -94,6 +94,7 @@ function Carousel({
React.useEffect(() => { React.useEffect(() => {
if (!api) return; if (!api) return;
// eslint-disable-next-line react-hooks/set-state-in-effect -- carousel API callback, setState via onSelect
onSelect(api); onSelect(api);
api.on("reInit", onSelect); api.on("reInit", onSelect);
api.on("select", onSelect); api.on("select", onSelect);

View file

@ -13,6 +13,7 @@ export function useIsMobile() {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
}; };
mql.addEventListener("change", onChange); 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); setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange); return () => mql.removeEventListener("change", onChange);
}, []); }, []);

View file

@ -13,6 +13,7 @@ export function useIsMobile() {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
}; };
mql.addEventListener("change", onChange); 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); setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange); return () => mql.removeEventListener("change", onChange);
}, []); }, []);