From 69cda3353285e33c05d61e7543eabd06704ae7af Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Mon, 11 Aug 2025 23:52:32 +0100 Subject: [PATCH] Agent logs should be collapsable (vibe-kanban) (#451) * Add collapsible agent log headers - Make process headers (Setup Script, Coding Agent, Cleanup Script) clickable to hide/show logs - Add chevron icon with rotation animation to indicate collapsed state - Filter log entries to hide those from collapsed processes while preserving virtualization - Maintain scroll position and follow-output behavior when toggling collapse - Support keyboard navigation (Enter/Space) for accessibility Amp-Thread: https://ampcode.com/threads/T-f44b0256-de19-45e1-96cb-df755553716d Co-authored-by: Amp * Cleanup script changes for task attempt 990e23da-53ff-4203-ac58-f5b28653bc1f * Commit changes from coding agent for task attempt 990e23da-53ff-4203-ac58-f5b28653bc1f * Cleanup (and adjust lint) * Enhance agent log collapsibility with auto-hide and dev server filtering - Filter dev server processes from logs tab to reduce noise - Auto-collapse completed setup/cleanup scripts on initial load for cleaner UX - Auto-expand scripts that restart after completion - Add process constants for type safety and consistency - Separate auto-collapsed and user-collapsed state management - Maintain scroll position and user preferences throughout Amp-Thread: https://ampcode.com/threads/T-f44b0256-de19-45e1-96cb-df755553716d Co-authored-by: Amp * Cleanup script changes for task attempt 990e23da-53ff-4203-ac58-f5b28653bc1f * Add coding agent auto-collapse for cleaner log focus - Auto-collapse all non-latest coding agents on task load for focused viewing - Detect new coding agent starts (follow-ups) and collapse previous agents - Latest coding agent always remains expanded for active monitoring - Robust latest agent detection with timestamp tie-breaking - One-shot initial collapse prevents duplicate processing - Smart follow-up detection tracks new running agents - User manual toggles permanently override auto-collapse behavior - Comprehensive state reset on attempt changes Amp-Thread: https://ampcode.com/threads/T-f44b0256-de19-45e1-96cb-df755553716d Co-authored-by: Amp * Cleanup script changes for task attempt 990e23da-53ff-4203-ac58-f5b28653bc1f * Fix timing issue with coding agent auto-collapse - Only mark initial collapse as complete when coding agents are actually processed - Prevents race condition where flag was set before real data arrived - Ensures auto-collapse logic runs after data loads, not during empty state - Fixes issue where all coding agents remained expanded instead of latest-only Amp-Thread: https://ampcode.com/threads/T-f44b0256-de19-45e1-96cb-df755553716d Co-authored-by: Amp * Cleanup script changes for task attempt 990e23da-53ff-4203-ac58-f5b28653bc1f * refactor * lints * prettier --------- Co-authored-by: Amp --- frontend/.eslintrc.json | 30 +- frontend/package-lock.json | 52 +++- frontend/package.json | 1 + frontend/src/components/logs/LogEntryRow.tsx | 13 +- .../src/components/logs/ProcessStartCard.tsx | 36 ++- .../components/tasks/TaskDetails/LogsTab.tsx | 275 +++++++++++++++++- .../tasks/TaskDetails/ProcessCard.tsx | 8 +- frontend/src/constants/processes.ts | 62 ++++ pnpm-lock.yaml | 18 ++ 9 files changed, 453 insertions(+), 42 deletions(-) create mode 100644 frontend/src/constants/processes.ts diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index fc60d4a6..e66df566 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,15 +1,25 @@ { "root": true, - "env": { "browser": true, "es2020": true }, + "env": { + "browser": true, + "es2020": true + }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended", "prettier" ], - "ignorePatterns": ["dist", ".eslintrc.json"], + "ignorePatterns": [ + "dist", + ".eslintrc.json" + ], "parser": "@typescript-eslint/parser", - "plugins": ["react-refresh", "@typescript-eslint"], + "plugins": [ + "react-refresh", + "@typescript-eslint", + "unused-imports" + ], "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" @@ -17,9 +27,19 @@ "rules": { "react-refresh/only-export-components": [ "warn", - { "allowConstantExport": true } + { + "allowConstantExport": true + } + ], + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "error", + { + "vars": "all", + "args": "after-used", + "ignoreRestSiblings": false + } ], - "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-explicit-any": "warn" } } \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 173e86c3..495097bf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -40,7 +40,8 @@ "react-window": "^1.8.11", "rfc6902": "^5.1.2", "tailwind-merge": "^2.2.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "use-debounce": "^10.0.5" }, "devDependencies": { "@types/react": "^18.2.43", @@ -54,6 +55,7 @@ "eslint-plugin-prettier": "^5.5.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "eslint-plugin-unused-imports": "^4.1.4", "postcss": "^8.4.32", "prettier": "^3.6.1", "tailwindcss": "^3.4.0", @@ -1122,6 +1124,15 @@ "lowlight": "^3.3.0" } }, + "node_modules/@git-diff-view/file/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/@git-diff-view/lowlight": { "version": "0.0.30", "resolved": "https://registry.npmjs.org/@git-diff-view/lowlight/-/lowlight-0.0.30.tgz", @@ -3805,15 +3816,6 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, - "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diff": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", @@ -4081,6 +4083,22 @@ "eslint": ">=8.40" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz", + "integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -7441,6 +7459,18 @@ } } }, + "node_modules/use-debounce": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.5.tgz", + "integrity": "sha512-Q76E3lnIV+4YT9AHcrHEHYmAd9LKwUAbPXDm7FlqVGDHiSOhX3RDjT8dm0AxbJup6WgOb1YEcKyCr11kBJR5KQ==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", @@ -7778,4 +7808,4 @@ } } } -} \ No newline at end of file +} diff --git a/frontend/package.json b/frontend/package.json index a096ad6d..9e1746d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -60,6 +60,7 @@ "eslint-plugin-prettier": "^5.5.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "eslint-plugin-unused-imports": "^4.1.4", "postcss": "^8.4.32", "prettier": "^3.6.1", "tailwindcss": "^3.4.0", diff --git a/frontend/src/components/logs/LogEntryRow.tsx b/frontend/src/components/logs/LogEntryRow.tsx index c3a407a9..fbef19aa 100644 --- a/frontend/src/components/logs/LogEntryRow.tsx +++ b/frontend/src/components/logs/LogEntryRow.tsx @@ -11,9 +11,18 @@ interface LogEntryRowProps { index: number; style?: React.CSSProperties; setRowHeight?: (index: number, height: number) => void; + isCollapsed?: boolean; + onToggleCollapse?: (processId: string) => void; } -function LogEntryRow({ entry, index, style, setRowHeight }: LogEntryRowProps) { +function LogEntryRow({ + entry, + index, + style, + setRowHeight, + isCollapsed, + onToggleCollapse, +}: LogEntryRowProps) { const rowRef = useRef(null); useEffect(() => { @@ -42,6 +51,8 @@ function LogEntryRow({ entry, index, style, setRowHeight }: LogEntryRowProps) { return ( {})} /> ); default: diff --git a/frontend/src/components/logs/ProcessStartCard.tsx b/frontend/src/components/logs/ProcessStartCard.tsx index c0e02d65..fa07ffb9 100644 --- a/frontend/src/components/logs/ProcessStartCard.tsx +++ b/frontend/src/components/logs/ProcessStartCard.tsx @@ -1,11 +1,18 @@ -import { Clock, Cog, Play, Terminal, Code } from 'lucide-react'; +import { Clock, Cog, Play, Terminal, Code, ChevronDown } from 'lucide-react'; +import { cn } from '@/lib/utils'; import type { ProcessStartPayload } from '@/types/logs'; interface ProcessStartCardProps { payload: ProcessStartPayload; + isCollapsed: boolean; + onToggle: (processId: string) => void; } -function ProcessStartCard({ payload }: ProcessStartCardProps) { +function ProcessStartCard({ + payload, + isCollapsed, + onToggle, +}: ProcessStartCardProps) { const getProcessIcon = (runReason: string) => { switch (runReason) { case 'setupscript': @@ -40,9 +47,26 @@ function ProcessStartCard({ payload }: ProcessStartCardProps) { return new Date(dateString).toLocaleTimeString(); }; + const handleClick = () => { + onToggle(payload.processId); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onToggle(payload.processId); + } + }; + return (
-
+
{getProcessIcon(payload.runReason)} @@ -67,6 +91,12 @@ function ProcessStartCard({ payload }: ProcessStartCardProps) { > {payload.status}
+
diff --git a/frontend/src/components/tasks/TaskDetails/LogsTab.tsx b/frontend/src/components/tasks/TaskDetails/LogsTab.tsx index a660876a..fc7b819d 100644 --- a/frontend/src/components/tasks/TaskDetails/LogsTab.tsx +++ b/frontend/src/components/tasks/TaskDetails/LogsTab.tsx @@ -1,29 +1,275 @@ -import { useContext, useState, useRef, useCallback } from 'react'; +import { + useContext, + useRef, + useCallback, + useMemo, + useEffect, + useReducer, +} from 'react'; import { Virtuoso } from 'react-virtuoso'; import { Cog } from 'lucide-react'; -import { TaskAttemptDataContext } from '@/components/context/taskDetailsContext.ts'; +import { + TaskAttemptDataContext, + TaskSelectedAttemptContext, +} from '@/components/context/taskDetailsContext.ts'; import { useProcessesLogs } from '@/hooks/useProcessesLogs'; import LogEntryRow from '@/components/logs/LogEntryRow'; +import { + shouldShowInLogs, + isAutoCollapsibleProcess, + isProcessCompleted, + isCodingAgent, + getLatestCodingAgent, + PROCESS_STATUSES, +} from '@/constants/processes'; +import type { ExecutionProcessStatus } from 'shared/types'; + +// Helper functions +function addAll(set: Set, items: T[]): Set { + items.forEach((i: T) => set.add(i)); + return set; +} + +// State management types +type LogsState = { + userCollapsed: Set; + autoCollapsed: Set; + prevStatus: Map; + prevLatestAgent?: string; +}; + +type LogsAction = + | { type: 'RESET_ATTEMPT' } + | { type: 'TOGGLE_USER'; id: string } + | { type: 'AUTO_COLLAPSE'; ids: string[] } + | { type: 'AUTO_EXPAND'; ids: string[] } + | { type: 'UPDATE_STATUS'; id: string; status: ExecutionProcessStatus } + | { type: 'NEW_RUNNING_AGENT'; id: string }; + +const initialState: LogsState = { + userCollapsed: new Set(), + autoCollapsed: new Set(), + prevStatus: new Map(), + prevLatestAgent: undefined, +}; + +function reducer(state: LogsState, action: LogsAction): LogsState { + switch (action.type) { + case 'RESET_ATTEMPT': + return { ...initialState }; + + case 'TOGGLE_USER': { + const newUserCollapsed = new Set(state.userCollapsed); + const newAutoCollapsed = new Set(state.autoCollapsed); + + const isCurrentlyCollapsed = + newUserCollapsed.has(action.id) || newAutoCollapsed.has(action.id); + + if (isCurrentlyCollapsed) { + // we want to EXPAND + newUserCollapsed.delete(action.id); + newAutoCollapsed.delete(action.id); + } else { + // we want to COLLAPSE + newUserCollapsed.add(action.id); + } + + return { + ...state, + userCollapsed: newUserCollapsed, + autoCollapsed: newAutoCollapsed, + }; + } + + case 'AUTO_COLLAPSE': { + const newAutoCollapsed = new Set(state.autoCollapsed); + addAll(newAutoCollapsed, action.ids); + return { + ...state, + autoCollapsed: newAutoCollapsed, + }; + } + + case 'AUTO_EXPAND': { + const newAutoCollapsed = new Set(state.autoCollapsed); + action.ids.forEach((id) => newAutoCollapsed.delete(id)); + return { + ...state, + autoCollapsed: newAutoCollapsed, + }; + } + + case 'UPDATE_STATUS': { + const newPrevStatus = new Map(state.prevStatus); + newPrevStatus.set(action.id, action.status); + return { + ...state, + prevStatus: newPrevStatus, + }; + } + + case 'NEW_RUNNING_AGENT': + return { + ...state, + prevLatestAgent: action.id, + }; + + default: + return state; + } +} function LogsTab() { const { attemptData } = useContext(TaskAttemptDataContext); - const [isAtBottom, setIsAtBottom] = useState(true); + const { selectedAttempt } = useContext(TaskSelectedAttemptContext); const virtuosoRef = useRef(null); - const { entries } = useProcessesLogs(attemptData.processes || [], true); + const [state, dispatch] = useReducer(reducer, initialState); + + // Filter out dev server processes before passing to useProcessesLogs + const filteredProcesses = useMemo( + () => + (attemptData.processes || []).filter((process) => + shouldShowInLogs(process.run_reason) + ), + [attemptData.processes] + ); + + const { entries } = useProcessesLogs(filteredProcesses, true); + + // Combined collapsed processes (auto + user) + const allCollapsedProcesses = useMemo(() => { + const combined = new Set(state.autoCollapsed); + state.userCollapsed.forEach((id: string) => combined.add(id)); + return combined; + }, [state.autoCollapsed, state.userCollapsed]); + + // Toggle collapsed state for a process (user action) + const toggleProcessCollapse = useCallback((processId: string) => { + dispatch({ type: 'TOGGLE_USER', id: processId }); + }, []); + + // Effect #1: Reset state when attempt changes + useEffect(() => { + dispatch({ type: 'RESET_ATTEMPT' }); + }, [selectedAttempt?.id]); + + // Effect #2: Handle setup/cleanup script auto-collapse and auto-expand + useEffect(() => { + const toCollapse: string[] = []; + const toExpand: string[] = []; + + filteredProcesses.forEach((process) => { + if (isAutoCollapsibleProcess(process.run_reason)) { + const prevStatus = state.prevStatus.get(process.id); + const currentStatus = process.status; + + // Auto-collapse completed setup/cleanup scripts + const shouldAutoCollapse = + (prevStatus === PROCESS_STATUSES.RUNNING || + prevStatus === undefined) && + isProcessCompleted(currentStatus) && + !state.userCollapsed.has(process.id) && + !state.autoCollapsed.has(process.id); + + if (shouldAutoCollapse) { + toCollapse.push(process.id); + } + + // Auto-expand scripts that restart after completion + const becameRunningAgain = + prevStatus && + isProcessCompleted(prevStatus) && + currentStatus === PROCESS_STATUSES.RUNNING && + state.autoCollapsed.has(process.id); + + if (becameRunningAgain) { + toExpand.push(process.id); + } + + // Update status tracking + dispatch({ + type: 'UPDATE_STATUS', + id: process.id, + status: currentStatus, + }); + } + }); + + if (toCollapse.length > 0) { + dispatch({ type: 'AUTO_COLLAPSE', ids: toCollapse }); + } + + if (toExpand.length > 0) { + dispatch({ type: 'AUTO_EXPAND', ids: toExpand }); + } + }, [ + filteredProcesses, + state.userCollapsed, + state.autoCollapsed, + state.prevStatus, + ]); + + // Effect #3: Handle coding agent succession logic + useEffect(() => { + const latestCodingAgentId = getLatestCodingAgent(filteredProcesses); + if (!latestCodingAgentId) return; + + // Collapse previous agents when a new latest agent appears + if (latestCodingAgentId !== state.prevLatestAgent) { + // Collapse all other coding agents that aren't user-collapsed + const toCollapse = filteredProcesses + .filter( + (p) => + isCodingAgent(p.run_reason) && + p.id !== latestCodingAgentId && + !state.userCollapsed.has(p.id) && + !state.autoCollapsed.has(p.id) + ) + .map((p) => p.id); + + if (toCollapse.length > 0) { + dispatch({ type: 'AUTO_COLLAPSE', ids: toCollapse }); + } + + dispatch({ type: 'NEW_RUNNING_AGENT', id: latestCodingAgentId }); + } + }, [ + filteredProcesses, + state.prevLatestAgent, + state.userCollapsed, + state.autoCollapsed, + ]); + + // Filter entries to hide logs from collapsed processes + const visibleEntries = useMemo(() => { + return entries.filter((entry) => + entry.channel === 'process_start' + ? true + : !allCollapsedProcesses.has(entry.processId) + ); + }, [entries, allCollapsedProcesses]); // Memoized item content to prevent flickering const itemContent = useCallback( - (index: number, entry: any) => , - [] + (index: number, entry: any) => ( + + ), + [allCollapsedProcesses, toggleProcessCollapse] ); - // Handle when user manually scrolls away from bottom - const handleAtBottomStateChange = useCallback((atBottom: boolean) => { - setIsAtBottom(atBottom); - }, []); - - if (!attemptData.processes || attemptData.processes.length === 0) { + if (!filteredProcesses || filteredProcesses.length === 0) { return (
@@ -39,10 +285,9 @@ function LogsTab() { { - if (logEndRef.current) { - logEndRef.current.scrollIntoView({ behavior: 'smooth' }); - } - }, [logs, entries]); const getStatusIcon = (status: ExecutionProcessStatus) => { switch (status) { case 'running': diff --git a/frontend/src/constants/processes.ts b/frontend/src/constants/processes.ts new file mode 100644 index 00000000..5b957ea2 --- /dev/null +++ b/frontend/src/constants/processes.ts @@ -0,0 +1,62 @@ +import type { + ExecutionProcessRunReason, + ExecutionProcessStatus, + ExecutionProcessSummary, +} from 'shared/types'; + +// Process run reasons +export const PROCESS_RUN_REASONS = { + SETUP_SCRIPT: 'setupscript' as ExecutionProcessRunReason, + CLEANUP_SCRIPT: 'cleanupscript' as ExecutionProcessRunReason, + CODING_AGENT: 'codingagent' as ExecutionProcessRunReason, + DEV_SERVER: 'devserver' as ExecutionProcessRunReason, +} as const; + +// Process statuses +export const PROCESS_STATUSES = { + RUNNING: 'running' as ExecutionProcessStatus, + COMPLETED: 'completed' as ExecutionProcessStatus, + FAILED: 'failed' as ExecutionProcessStatus, + KILLED: 'killed' as ExecutionProcessStatus, +} as const; + +// Helper functions +export const isAutoCollapsibleProcess = ( + runReason: ExecutionProcessRunReason +): boolean => { + return ( + runReason === PROCESS_RUN_REASONS.SETUP_SCRIPT || + runReason === PROCESS_RUN_REASONS.CLEANUP_SCRIPT + ); +}; + +export const isCodingAgent = ( + runReason: ExecutionProcessRunReason +): boolean => { + return runReason === PROCESS_RUN_REASONS.CODING_AGENT; +}; + +export const isProcessCompleted = (status: ExecutionProcessStatus): boolean => { + return ( + status === PROCESS_STATUSES.COMPLETED || status === PROCESS_STATUSES.FAILED + ); +}; + +export const shouldShowInLogs = ( + runReason: ExecutionProcessRunReason +): boolean => { + return runReason !== PROCESS_RUN_REASONS.DEV_SERVER; +}; + +export const getLatestCodingAgent = ( + processes: ExecutionProcessSummary[] +): string | null => { + const codingAgents = processes.filter((p) => isCodingAgent(p.run_reason)); + if (codingAgents.length === 0) return null; + + return codingAgents.sort((a, b) => + a.started_at === b.started_at + ? a.id.localeCompare(b.id) // tie-break for same timestamp + : new Date(b.started_at).getTime() - new Date(a.started_at).getTime() + )[0].id; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5048b6f6..1d948cf9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,9 @@ importers: eslint-plugin-react-refresh: specifier: ^0.4.5 version: 0.4.20(eslint@8.57.1) + eslint-plugin-unused-imports: + specifier: ^4.1.4 + version: 4.1.4(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1) postcss: specifier: ^8.4.32 version: 8.5.6 @@ -1719,6 +1722,15 @@ packages: peerDependencies: eslint: '>=8.40' + eslint-plugin-unused-imports@4.1.4: + resolution: {integrity: sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4419,6 +4431,12 @@ snapshots: dependencies: eslint: 8.57.1 + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0