Skip to content

Commit 65dba1a

Browse files
committed
Add Tasks panel infrastructure with type-safe IPC protocol
Adds foundational infrastructure for the Tasks panel: Backend (TasksPanel.ts): - CRUD operations for tasks (create, delete, pause, resume) - Template and preset management - Log fetching with caching - Real-time push notifications to webview Type-safe IPC Protocol: - Generic request/response/notification patterns - Compile-time type safety for webview-extension messages - useIpc hook for React components - useTasksApi hook with typed methods Supporting infrastructure: - Test setup for jsdom compatibility with Lit elements - Codicon stylesheet integration for vscode-elements - React Compiler integration with ESLint plugin - pnpm catalog for consistent dependency versions The UI components will be added in subsequent PRs.
1 parent 2bd2cc7 commit 65dba1a

File tree

27 files changed

+3355
-1140
lines changed

27 files changed

+3355
-1140
lines changed

esbuild.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ const buildOptions = {
2727
target: "node20",
2828
format: "cjs",
2929
mainFields: ["module", "main"],
30-
// Force openpgp to use CJS. The ESM version uses import.meta.url which is
31-
// undefined when bundled to CJS, causing runtime errors.
3230
alias: {
31+
// Force openpgp to use CJS. The ESM version uses import.meta.url which is
32+
// undefined when bundled to CJS, causing runtime errors.
3333
openpgp: "./node_modules/openpgp/dist/node/openpgp.min.cjs",
34+
"@repo/webview-shared": "./packages/webview-shared/src/index.ts",
3435
},
3536
external: ["vscode"],
3637
sourcemap: production ? "external" : true,

eslint.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { createTypeScriptImportResolver } from "eslint-import-resolver-typescrip
88
import { flatConfigs as importXFlatConfigs } from "eslint-plugin-import-x";
99
import packageJson from "eslint-plugin-package-json";
1010
import reactPlugin from "eslint-plugin-react";
11+
import reactCompilerPlugin from "eslint-plugin-react-compiler";
1112
import reactHooksPlugin from "eslint-plugin-react-hooks";
1213
import globals from "globals";
1314

@@ -181,6 +182,7 @@ export default defineConfig(
181182
files: ["**/*.tsx"],
182183
plugins: {
183184
react: reactPlugin,
185+
"react-compiler": reactCompilerPlugin,
184186
"react-hooks": reactHooksPlugin,
185187
},
186188
settings: {
@@ -194,6 +196,7 @@ export default defineConfig(
194196
...reactPlugin.configs["jsx-runtime"].rules, // React 17+ JSX transform
195197
...reactHooksPlugin.configs.recommended.rules,
196198
"react/prop-types": "off", // Using TypeScript
199+
"react-compiler/react-compiler": "error",
197200
},
198201
},
199202

package.json

Lines changed: 73 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@
182182
"id": "coder",
183183
"title": "Coder Remote",
184184
"icon": "media/logo-white.svg"
185+
},
186+
{
187+
"id": "coderTasks",
188+
"title": "Coder Tasks",
189+
"icon": "media/tasks-logo.svg"
185190
}
186191
]
187192
},
@@ -199,13 +204,14 @@
199204
"visibility": "visible",
200205
"icon": "media/logo-white.svg",
201206
"when": "coder.authenticated && coder.isOwner"
202-
},
207+
}
208+
],
209+
"coderTasks": [
203210
{
204211
"type": "webview",
205212
"id": "coder.tasksPanel",
206-
"name": "Tasks",
207-
"icon": "media/logo-white.svg",
208-
"when": "coder.authenticated && coder.devMode"
213+
"name": "Coder Tasks",
214+
"when": "coder.authenticated"
209215
}
210216
]
211217
},
@@ -308,6 +314,12 @@
308314
"command": "coder.manageCredentials",
309315
"title": "Manage Credentials",
310316
"category": "Coder"
317+
},
318+
{
319+
"command": "coder.tasks.refresh",
320+
"title": "Refresh Tasks",
321+
"category": "Coder",
322+
"icon": "$(refresh)"
311323
}
312324
],
313325
"menus": {
@@ -370,6 +382,10 @@
370382
},
371383
{
372384
"command": "coder.manageCredentials"
385+
},
386+
{
387+
"command": "coder.tasks.refresh",
388+
"when": "false"
373389
}
374390
],
375391
"view/title": [
@@ -404,6 +420,11 @@
404420
"command": "coder.searchAllWorkspaces",
405421
"when": "coder.authenticated && view == allWorkspaces",
406422
"group": "navigation@3"
423+
},
424+
{
425+
"command": "coder.tasks.refresh",
426+
"when": "coder.authenticated && view == coder.tasksPanel",
427+
"group": "navigation@1"
407428
}
408429
],
409430
"view/item/context": [
@@ -463,52 +484,54 @@
463484
"zod": "^4.3.6"
464485
},
465486
"devDependencies": {
466-
"@eslint/js": "^9.39.2",
467-
"@eslint/markdown": "^7.5.1",
468-
"@testing-library/react": "^16.3.2",
469-
"@tsconfig/node20": "^20.1.8",
470-
"@types/mocha": "^10.0.10",
471-
"@types/node": "^20",
472-
"@types/proper-lockfile": "^4.1.4",
473-
"@types/react": "catalog:",
474-
"@types/react-dom": "catalog:",
475-
"@types/semver": "^7.7.1",
476-
"@types/ua-parser-js": "0.7.39",
477-
"@types/vscode": "^1.95.0",
478-
"@types/ws": "^8.18.1",
479-
"@typescript-eslint/eslint-plugin": "^8.53.1",
480-
"@typescript-eslint/parser": "^8.53.1",
481-
"@vitejs/plugin-react-swc": "catalog:",
482-
"@vitest/coverage-v8": "^4.0.16",
483-
"@vscode/test-cli": "^0.0.12",
484-
"@vscode/test-electron": "^2.5.2",
485-
"@vscode/vsce": "^3.7.1",
486-
"bufferutil": "^4.1.0",
487-
"coder": "github:coder/coder#main",
488-
"concurrently": "^9.2.1",
489-
"dayjs": "^1.11.19",
490-
"electron": "^40.0.0",
491-
"esbuild": "^0.27.2",
492-
"eslint": "^9.39.2",
493-
"eslint-config-prettier": "^10.1.8",
494-
"eslint-import-resolver-typescript": "^4.4.4",
495-
"eslint-plugin-import-x": "^4.16.1",
496-
"eslint-plugin-package-json": "^0.88.2",
497-
"eslint-plugin-react": "^7.37.0",
498-
"eslint-plugin-react-hooks": "^5.0.0",
499-
"globals": "^17.0.0",
500-
"jsdom": "^27.4.0",
501-
"jsonc-eslint-parser": "^2.4.2",
502-
"memfs": "^4.56.10",
503-
"prettier": "^3.7.4",
504-
"react": "catalog:",
505-
"react-dom": "catalog:",
506-
"typescript": "catalog:",
507-
"typescript-eslint": "^8.53.1",
508-
"utf-8-validate": "^6.0.6",
509-
"vite": "catalog:",
510-
"vitest": "^4.0.16"
511-
},
487+
"@eslint/js": "^9.39.2",
488+
"@eslint/markdown": "^7.5.1",
489+
"@testing-library/react": "^16.3.2",
490+
"@tsconfig/node20": "^20.1.8",
491+
"@types/mocha": "^10.0.10",
492+
"@types/node": "^20",
493+
"@types/proper-lockfile": "^4.1.4",
494+
"@types/react": "catalog:",
495+
"@types/react-dom": "catalog:",
496+
"@types/semver": "^7.7.1",
497+
"@types/ua-parser-js": "0.7.39",
498+
"@types/vscode": "^1.95.0",
499+
"@types/ws": "^8.18.1",
500+
"@typescript-eslint/eslint-plugin": "^8.53.1",
501+
"@typescript-eslint/parser": "^8.53.1",
502+
"@vitejs/plugin-react": "catalog:",
503+
"@vitest/coverage-v8": "^4.0.16",
504+
"@vscode/test-cli": "^0.0.12",
505+
"@vscode/test-electron": "^2.5.2",
506+
"@vscode/vsce": "^3.7.1",
507+
"babel-plugin-react-compiler": "catalog:",
508+
"bufferutil": "^4.1.0",
509+
"coder": "github:coder/coder#main",
510+
"concurrently": "^9.2.1",
511+
"dayjs": "^1.11.19",
512+
"electron": "^40.0.0",
513+
"esbuild": "^0.27.2",
514+
"eslint": "^9.39.2",
515+
"eslint-config-prettier": "^10.1.8",
516+
"eslint-import-resolver-typescript": "^4.4.4",
517+
"eslint-plugin-import-x": "^4.16.1",
518+
"eslint-plugin-package-json": "^0.88.2",
519+
"eslint-plugin-react": "^7.37.0",
520+
"eslint-plugin-react-compiler": "catalog:",
521+
"eslint-plugin-react-hooks": "^5.0.0",
522+
"globals": "^17.0.0",
523+
"jsdom": "^27.4.0",
524+
"jsonc-eslint-parser": "^2.4.2",
525+
"memfs": "^4.56.10",
526+
"prettier": "^3.7.4",
527+
"react": "catalog:",
528+
"react-dom": "catalog:",
529+
"typescript": "catalog:",
530+
"typescript-eslint": "^8.53.1",
531+
"utf-8-validate": "^6.0.6",
532+
"vite": "catalog:",
533+
"vitest": "^4.0.16"
534+
},
512535
"extensionPack": [
513536
"ms-vscode-remote.remote-ssh"
514537
],

packages/tasks/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
"dependencies": {
1212
"@repo/webview-shared": "workspace:*",
1313
"@vscode-elements/react-elements": "catalog:",
14+
"@vscode/codicons": "catalog:",
1415
"react": "catalog:",
1516
"react-dom": "catalog:"
1617
},
1718
"devDependencies": {
1819
"@types/react": "catalog:",
1920
"@types/react-dom": "catalog:",
20-
"@vitejs/plugin-react-swc": "catalog:",
21+
"@vitejs/plugin-react": "catalog:",
22+
"babel-plugin-react-compiler": "catalog:",
2123
"typescript": "catalog:",
2224
"vite": "catalog:"
2325
}

packages/tasks/src/App.tsx

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,62 @@
1-
import { logger } from "@repo/webview-shared/logger";
2-
import { useMessage } from "@repo/webview-shared/react";
3-
import {
4-
VscodeButton,
5-
VscodeProgressRing,
6-
} from "@vscode-elements/react-elements";
1+
import { useTasksApi } from "@repo/webview-shared/react";
2+
import { VscodeProgressRing } from "@vscode-elements/react-elements";
73
import { useEffect, useState } from "react";
84

9-
import { sendReady, sendRefresh } from "./messages";
10-
11-
import type { TasksExtensionMessage } from "@repo/webview-shared";
5+
import type { Task, TaskTemplate } from "@repo/webview-shared";
126

137
export default function App() {
14-
const [ready, setReady] = useState(false);
15-
16-
useMessage<TasksExtensionMessage>((message) => {
17-
switch (message.type) {
18-
case "init":
19-
setReady(true);
20-
break;
21-
case "error":
22-
logger.error("Tasks panel error:", message.data);
23-
break;
24-
}
25-
});
8+
const api = useTasksApi();
9+
const [loading, setLoading] = useState(true);
10+
const [error, setError] = useState<string | null>(null);
11+
const [tasks, setTasks] = useState<Task[]>([]);
12+
const [templates, setTemplates] = useState<TaskTemplate[]>([]);
13+
const [tasksSupported, setTasksSupported] = useState(true);
2614

2715
useEffect(() => {
28-
sendReady();
29-
}, []);
16+
api
17+
.init()
18+
.then((data) => {
19+
setTasks(data.tasks);
20+
setTemplates(data.templates);
21+
setTasksSupported(data.tasksSupported);
22+
setLoading(false);
23+
})
24+
.catch((err) => {
25+
setError(err instanceof Error ? err.message : "Failed to initialize");
26+
setLoading(false);
27+
});
28+
}, [api]);
29+
30+
if (loading) {
31+
return (
32+
<div className="loading-container">
33+
<VscodeProgressRing />
34+
</div>
35+
);
36+
}
37+
38+
if (error) {
39+
return (
40+
<div className="error-container">
41+
<p>Error: {error}</p>
42+
</div>
43+
);
44+
}
3045

31-
if (!ready) {
32-
return <VscodeProgressRing />;
46+
if (!tasksSupported) {
47+
return (
48+
<div className="not-supported">
49+
<p>Tasks are not supported on this Coder server.</p>
50+
</div>
51+
);
3352
}
3453

3554
return (
36-
<div>
37-
<h2>Coder Tasks</h2>
38-
<VscodeButton onClick={sendRefresh}>Refresh</VscodeButton>
55+
<div className="tasks-panel">
56+
<h3>Tasks Infrastructure Ready</h3>
57+
<p>Templates: {templates.length}</p>
58+
<p>Tasks: {tasks.length}</p>
59+
<p>Full UI coming in next PR...</p>
3960
</div>
4061
);
4162
}

packages/tasks/src/messages.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

packages/webview-shared/createWebviewConfig.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import react from "@vitejs/plugin-react-swc";
1+
import react from "@vitejs/plugin-react";
22
import { resolve } from "node:path";
33
import { defineConfig, type UserConfig } from "vite";
44

@@ -14,7 +14,15 @@ export function createWebviewConfig(
1414
const production = process.env.NODE_ENV === "production";
1515

1616
return defineConfig({
17-
plugins: [react()],
17+
plugins: [
18+
react({
19+
babel: {
20+
plugins: [["babel-plugin-react-compiler", {}]],
21+
},
22+
}),
23+
],
24+
// Use relative paths for assets (required for VS Code webviews)
25+
base: "./",
1826
build: {
1927
outDir: resolve(dirname, `../../dist/webviews/${webviewName}`),
2028
emptyOutDir: true,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
// Types exposed to the extension (react/ subpath is excluded).
22
export type {
3+
LogsStatus,
4+
TaskActions,
5+
TaskDetails,
36
TasksExtensionMessage,
7+
TasksPushMessage,
8+
TasksRequest,
9+
TasksResponse,
410
TasksWebviewMessage,
11+
TaskTemplate,
12+
TaskUIState,
513
WebviewMessage,
614
} from "./src/index";
15+
16+
export {
17+
getTaskActions,
18+
getTaskUIState,
19+
isTasksRequest,
20+
isTasksResponse,
21+
} from "./src/index";

packages/webview-shared/src/api.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ function getVsCodeApi(): WebviewApi<unknown> {
1010
return vscodeApi;
1111
}
1212

13-
export function postMessage(message: WebviewMessage): void {
13+
/**
14+
* Post a message to the extension.
15+
* Accepts legacy WebviewMessage format or any object for the new IPC protocol.
16+
*/
17+
export function postMessage(message: WebviewMessage | Record<string, unknown>): void {
1418
getVsCodeApi().postMessage(message);
1519
}
1620

0 commit comments

Comments
 (0)