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:
parent
b0a8f32a10
commit
2d34d3a386
13 changed files with 24 additions and 6 deletions
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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't match the expected format for {channel}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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's get your workspace ready. This takes about a minute.
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
{/* Steps preview */}
|
{/* Steps preview */}
|
||||||
|
|
|
||||||
|
|
@ -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 "{filter}"
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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'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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue