diff --git a/eslint.config.mjs b/eslint.config.mjs
index 87839fc7..513b6326 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,30 +1,26 @@
// @ts-check
import eslint from "@eslint/js";
-import { defineConfig } from "eslint/config";
+import { defineConfig, globalIgnores } from "eslint/config";
import markdown from "@eslint/markdown";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
import { flatConfigs as importXFlatConfigs } from "eslint-plugin-import-x";
import packageJson from "eslint-plugin-package-json";
-import reactPlugin from "eslint-plugin-react";
-import reactHooksPlugin from "eslint-plugin-react-hooks";
+import eslintReact from "@eslint-react/eslint-plugin";
import globals from "globals";
export default defineConfig(
- // Global ignores
- {
- ignores: [
- "out/**",
- "dist/**",
- "packages/*/dist/**",
- "**/*.d.ts",
- "vitest.config.ts",
- "**/vite.config*.ts",
- "**/createWebviewConfig.ts",
- ".vscode-test/**",
- ],
- },
+ globalIgnores([
+ "out/**",
+ "dist/**",
+ "packages/*/dist/**",
+ "**/*.d.ts",
+ "vitest.config.ts",
+ "**/vite.config*.ts",
+ "**/createWebviewConfig.ts",
+ ".vscode-test/**",
+ ]),
// Base ESLint recommended rules (for JS/TS/TSX files only)
{
@@ -176,38 +172,31 @@ export default defineConfig(
},
},
- // React hooks and compiler rules (covers .ts hook files too)
+ // React rules with type-checked analysis (covers hooks, JSX, DOM)
{
files: ["packages/**/*.{ts,tsx}"],
- ...reactHooksPlugin.configs.flat.recommended,
+ extends: [eslintReact.configs["recommended-type-checked"]],
rules: {
- ...reactHooksPlugin.configs.flat.recommended.rules,
// React Compiler auto-memoizes; exhaustive-deps false-positives on useCallback
- "react-hooks/exhaustive-deps": "off",
+ "@eslint-react/exhaustive-deps": "off",
},
},
- // TSX files - React JSX rules
+ // Package.json linting
+ packageJson.configs.recommended,
{
- files: ["**/*.tsx"],
- plugins: {
- react: reactPlugin,
- },
- settings: {
- react: {
- version: "detect",
- },
- },
+ // The root package.json is a VS Code extension (not an npm package),
+ // so these publishing-oriented rules don't apply.
+ files: ["package.json"],
+ ignores: ["packages/**/package.json"],
rules: {
- ...reactPlugin.configs.recommended.rules,
- ...reactPlugin.configs["jsx-runtime"].rules, // React 17+ JSX transform
- "react/prop-types": "off", // Using TypeScript
+ "package-json/require-exports": "off",
+ "package-json/require-files": "off",
+ "package-json/require-sideEffects": "off",
+ "package-json/require-attribution": "off",
},
},
- // Package.json linting
- packageJson.configs.recommended,
-
// Markdown linting with GitHub-flavored admonitions allowed
...markdown.configs.recommended,
{
diff --git a/package.json b/package.json
index b8885154..7b749db1 100644
--- a/package.json
+++ b/package.json
@@ -473,23 +473,24 @@
"dependencies": {
"@peculiar/x509": "^1.14.3",
"@repo/shared": "workspace:*",
- "axios": "1.13.5",
+ "axios": "1.13.6",
"date-fns": "^4.1.0",
"eventsource": "^4.1.0",
- "find-process": "^2.0.0",
+ "find-process": "^2.1.0",
"jsonc-parser": "^3.3.1",
"openpgp": "^6.3.0",
"pretty-bytes": "^7.1.0",
"proper-lockfile": "^4.1.2",
"proxy-agent": "^6.5.0",
"semver": "^7.7.4",
- "strip-ansi": "^7.1.2",
+ "strip-ansi": "^7.2.0",
"ua-parser-js": "^1.0.41",
"ws": "^8.19.0",
"zod": "^4.3.6"
},
"devDependencies": {
- "@eslint/js": "^9.39.2",
+ "@eslint-react/eslint-plugin": "^2.13.0",
+ "@eslint/js": "^10.0.1",
"@eslint/markdown": "^7.5.1",
"@tanstack/react-query": "catalog:",
"@testing-library/jest-dom": "^6.9.1",
@@ -504,8 +505,8 @@
"@types/ua-parser-js": "0.7.39",
"@types/vscode": "^1.95.0",
"@types/ws": "^8.18.1",
- "@typescript-eslint/eslint-plugin": "^8.56.0",
- "@typescript-eslint/parser": "^8.56.0",
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
+ "@typescript-eslint/parser": "^8.56.1",
"@vitejs/plugin-react": "catalog:",
"@vitest/coverage-v8": "^4.0.18",
"@vscode/test-cli": "^0.0.12",
@@ -516,24 +517,22 @@
"coder": "catalog:",
"concurrently": "^9.2.1",
"dayjs": "^1.11.19",
- "electron": "^40.4.1",
+ "electron": "^40.6.1",
"esbuild": "^0.27.3",
- "eslint": "^9.39.2",
+ "eslint": "^10.0.2",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
- "eslint-plugin-package-json": "^0.88.3",
- "eslint-plugin-react": "^7.37.5",
- "eslint-plugin-react-hooks": "^7.0.1",
- "globals": "^17.3.0",
+ "eslint-plugin-package-json": "^0.89.2",
+ "globals": "^17.4.0",
"jsdom": "^28.1.0",
- "jsonc-eslint-parser": "^2.4.2",
+ "jsonc-eslint-parser": "^3.1.0",
"memfs": "^4.56.10",
"prettier": "^3.8.1",
"react": "catalog:",
"react-dom": "catalog:",
"typescript": "catalog:",
- "typescript-eslint": "^8.56.0",
+ "typescript-eslint": "^8.56.1",
"utf-8-validate": "^6.0.6",
"vite": "catalog:",
"vitest": "^4.0.18"
@@ -541,7 +540,7 @@
"extensionPack": [
"ms-vscode-remote.remote-ssh"
],
- "packageManager": "pnpm@10.29.2",
+ "packageManager": "pnpm@10.30.3",
"engines": {
"vscode": "^1.95.0",
"node": ">= 20"
diff --git a/packages/tasks/src/components/ActionMenu.tsx b/packages/tasks/src/components/ActionMenu.tsx
index 33c7c378..849b0614 100644
--- a/packages/tasks/src/components/ActionMenu.tsx
+++ b/packages/tasks/src/components/ActionMenu.tsx
@@ -74,39 +74,44 @@ export function ActionMenu({ items }: ActionMenuProps) {
tabIndex={-1}
onKeyDown={(e) => isEscape(e) && close()}
>
- {items.map((item, index) =>
- item.separator ? (
-
- ) : (
-
- ),
- )}
+ {items
+ .map((item, i) => ({
+ item,
+ key: item.separator ? `separator-${i}` : `${item.label}-${i}`,
+ }))
+ .map(({ item, key }) =>
+ item.separator ? (
+
+ ) : (
+
+ ),
+ )}
)}
diff --git a/packages/tasks/src/components/WorkspaceLogs.tsx b/packages/tasks/src/components/WorkspaceLogs.tsx
index 935e9598..dc2cf1d8 100644
--- a/packages/tasks/src/components/WorkspaceLogs.tsx
+++ b/packages/tasks/src/components/WorkspaceLogs.tsx
@@ -19,7 +19,7 @@ export function WorkspaceLogs({ task }: { task: Task }) {
{lines.length === 0 ? (
Waiting for logs...
) : (
- lines.map((line, i) => {line})
+ lines.map((entry) => {entry.text})
)}
);
diff --git a/packages/tasks/src/hooks/useFollowScroll.ts b/packages/tasks/src/hooks/useFollowScroll.ts
index 5a10ede6..7198366a 100644
--- a/packages/tasks/src/hooks/useFollowScroll.ts
+++ b/packages/tasks/src/hooks/useFollowScroll.ts
@@ -17,7 +17,7 @@ interface ScrollableElement extends HTMLElement {
*/
export function useFollowScroll(): RefObject {
const ref = useRef(null);
- const atBottom = useRef(true);
+ const atBottomRef = useRef(true);
useEffect(() => {
const sentinel = ref.current;
@@ -28,7 +28,7 @@ export function useFollowScroll(): RefObject {
const container = parent as ScrollableElement;
function onScroll() {
- atBottom.current =
+ atBottomRef.current =
container.scrollMax - container.scrollPos <= BOTTOM_THRESHOLD;
}
@@ -41,7 +41,7 @@ export function useFollowScroll(): RefObject {
});
const mo = new MutationObserver(() => {
- if (atBottom.current) {
+ if (atBottomRef.current) {
scrollToBottom();
}
});
diff --git a/packages/tasks/src/hooks/useWorkspaceLogs.ts b/packages/tasks/src/hooks/useWorkspaceLogs.ts
index f1589bd0..82bee722 100644
--- a/packages/tasks/src/hooks/useWorkspaceLogs.ts
+++ b/packages/tasks/src/hooks/useWorkspaceLogs.ts
@@ -2,14 +2,19 @@ import { useEffect, useState } from "react";
import { useTasksApi } from "./useTasksApi";
+export interface LogEntry {
+ id: number;
+ text: string;
+}
+
/**
* Subscribes to workspace log lines pushed from the extension.
* Batches updates per animation frame to avoid excessive re-renders
* when many lines arrive in quick succession.
*/
-export function useWorkspaceLogs(): string[] {
+export function useWorkspaceLogs(): LogEntry[] {
const { onWorkspaceLogsAppend, stopStreamingWorkspaceLogs } = useTasksApi();
- const [lines, setLines] = useState([]);
+ const [lines, setLines] = useState([]);
useEffect(() => {
let pending: string[] = [];
@@ -22,7 +27,13 @@ export function useWorkspaceLogs(): string[] {
const batch = pending;
pending = [];
frame = 0;
- setLines((prev) => prev.concat(batch));
+ setLines((prev) => {
+ const entries = batch.map((text, i) => ({
+ id: prev.length + i,
+ text,
+ }));
+ return prev.concat(entries);
+ });
});
}
});
diff --git a/packages/webview-shared/createWebviewConfig.ts b/packages/webview-shared/createWebviewConfig.ts
index 9ebbcdf9..c28b20cd 100644
--- a/packages/webview-shared/createWebviewConfig.ts
+++ b/packages/webview-shared/createWebviewConfig.ts
@@ -30,6 +30,8 @@ export function createWebviewConfig(
target: "esnext",
// Skip gzip size calculation for faster builds
reportCompressedSize: false,
+ // Webviews load as a single bundle; code-splitting doesn't apply
+ chunkSizeWarningLimit: 600,
rollupOptions: {
// HTML is generated by the extension with CSP headers
input: resolve(dirname, "src/index.tsx"),
diff --git a/packages/webview-shared/src/react/hooks.ts b/packages/webview-shared/src/react/hooks.ts
index 4c9cc02b..82718b83 100644
--- a/packages/webview-shared/src/react/hooks.ts
+++ b/packages/webview-shared/src/react/hooks.ts
@@ -22,7 +22,7 @@ export function useMessage(handler: (message: T) => void): void {
* Hook to manage webview state with VS Code's state API
*/
export function useVsCodeState(initialState: T): [T, (state: T) => void] {
- const [state, setLocalState] = useState((): T => {
+ const [localState, setLocalState] = useState((): T => {
const saved = getState();
return saved ?? initialState;
});
@@ -32,5 +32,5 @@ export function useVsCodeState(initialState: T): [T, (state: T) => void] {
setState(newState);
}, []);
- return [state, setVsCodeState];
+ return [localState, setVsCodeState];
}
diff --git a/packages/webview-shared/src/react/useIpc.ts b/packages/webview-shared/src/react/useIpc.ts
index 58f8c97c..b41c3a73 100644
--- a/packages/webview-shared/src/react/useIpc.ts
+++ b/packages/webview-shared/src/react/useIpc.ts
@@ -43,20 +43,20 @@ type NotificationHandler = (data: unknown) => void;
*/
export function useIpc(options: UseIpcOptions = {}) {
const { timeoutMs = DEFAULT_TIMEOUT_MS } = options;
- const pendingRequests = useRef