Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 25 additions & 36 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -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)
{
Expand Down Expand Up @@ -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,
{
Expand Down
29 changes: 14 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -516,32 +517,30 @@
"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"
},
"extensionPack": [
"ms-vscode-remote.remote-ssh"
],
"packageManager": "pnpm@10.29.2",
"packageManager": "pnpm@10.30.3",
"engines": {
"vscode": "^1.95.0",
"node": ">= 20"
Expand Down
71 changes: 38 additions & 33 deletions packages/tasks/src/components/ActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,39 +74,44 @@ export function ActionMenu({ items }: ActionMenuProps) {
tabIndex={-1}
onKeyDown={(e) => isEscape(e) && close()}
>
{items.map((item, index) =>
item.separator ? (
<div
key={`sep-${index}`}
className="action-menu-separator"
role="separator"
/>
) : (
<button
key={`${item.label}-${index}`}
type="button"
className={[
"action-menu-item",
item.danger && "danger",
item.loading && "loading",
]
.filter(Boolean)
.join(" ")}
onClick={() => {
item.onClick();
close();
}}
disabled={item.disabled === true || item.loading === true}
>
{item.loading ? (
<VscodeProgressRing className="action-menu-spinner" />
) : (
<VscodeIcon name={item.icon} className="action-menu-icon" />
)}
<span>{item.label}</span>
</button>
),
)}
{items
.map((item, i) => ({
item,
key: item.separator ? `separator-${i}` : `${item.label}-${i}`,
}))
.map(({ item, key }) =>
item.separator ? (
<div
key={key}
className="action-menu-separator"
role="separator"
/>
) : (
<button
key={key}
type="button"
className={[
"action-menu-item",
item.danger && "danger",
item.loading && "loading",
]
.filter(Boolean)
.join(" ")}
onClick={() => {
item.onClick();
close();
}}
disabled={item.disabled === true || item.loading === true}
>
{item.loading ? (
<VscodeProgressRing className="action-menu-spinner" />
) : (
<VscodeIcon name={item.icon} className="action-menu-icon" />
)}
<span>{item.label}</span>
</button>
),
)}
</div>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/tasks/src/components/WorkspaceLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function WorkspaceLogs({ task }: { task: Task }) {
{lines.length === 0 ? (
<LogViewerPlaceholder>Waiting for logs...</LogViewerPlaceholder>
) : (
lines.map((line, i) => <LogLine key={i}>{line}</LogLine>)
lines.map((entry) => <LogLine key={entry.id}>{entry.text}</LogLine>)
)}
</LogViewer>
);
Expand Down
6 changes: 3 additions & 3 deletions packages/tasks/src/hooks/useFollowScroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface ScrollableElement extends HTMLElement {
*/
export function useFollowScroll(): RefObject<HTMLDivElement | null> {
const ref = useRef<HTMLDivElement>(null);
const atBottom = useRef(true);
const atBottomRef = useRef(true);

useEffect(() => {
const sentinel = ref.current;
Expand All @@ -28,7 +28,7 @@ export function useFollowScroll(): RefObject<HTMLDivElement | null> {
const container = parent as ScrollableElement;

function onScroll() {
atBottom.current =
atBottomRef.current =
container.scrollMax - container.scrollPos <= BOTTOM_THRESHOLD;
}

Expand All @@ -41,7 +41,7 @@ export function useFollowScroll(): RefObject<HTMLDivElement | null> {
});

const mo = new MutationObserver(() => {
if (atBottom.current) {
if (atBottomRef.current) {
scrollToBottom();
}
});
Expand Down
17 changes: 14 additions & 3 deletions packages/tasks/src/hooks/useWorkspaceLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>([]);
const [lines, setLines] = useState<LogEntry[]>([]);

useEffect(() => {
let pending: string[] = [];
Expand All @@ -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);
});
});
}
});
Expand Down
2 changes: 2 additions & 0 deletions packages/webview-shared/createWebviewConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
4 changes: 2 additions & 2 deletions packages/webview-shared/src/react/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function useMessage<T>(handler: (message: T) => void): void {
* Hook to manage webview state with VS Code's state API
*/
export function useVsCodeState<T>(initialState: T): [T, (state: T) => void] {
const [state, setLocalState] = useState<T>((): T => {
const [localState, setLocalState] = useState<T>((): T => {
const saved = getState<T>();
return saved ?? initialState;
});
Expand All @@ -32,5 +32,5 @@ export function useVsCodeState<T>(initialState: T): [T, (state: T) => void] {
setState(newState);
}, []);

return [state, setVsCodeState];
return [localState, setVsCodeState];
}
Loading