Skip to content

Commit 360d305

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
1 parent 2bd2cc7 commit 360d305

File tree

28 files changed

+3338
-1095
lines changed

28 files changed

+3338
-1095
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

media/tasks-logo.svg

Lines changed: 4 additions & 0 deletions
Loading

package.json

Lines changed: 29 additions & 5 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,15 @@
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+
"icon": "media/tasks-logo.svg",
215+
"when": "coder.authenticated"
209216
}
210217
]
211218
},
@@ -308,6 +315,12 @@
308315
"command": "coder.manageCredentials",
309316
"title": "Manage Credentials",
310317
"category": "Coder"
318+
},
319+
{
320+
"command": "coder.tasks.refresh",
321+
"title": "Refresh Tasks",
322+
"category": "Coder",
323+
"icon": "$(refresh)"
311324
}
312325
],
313326
"menus": {
@@ -370,6 +383,10 @@
370383
},
371384
{
372385
"command": "coder.manageCredentials"
386+
},
387+
{
388+
"command": "coder.tasks.refresh",
389+
"when": "false"
373390
}
374391
],
375392
"view/title": [
@@ -404,6 +421,11 @@
404421
"command": "coder.searchAllWorkspaces",
405422
"when": "coder.authenticated && view == allWorkspaces",
406423
"group": "navigation@3"
424+
},
425+
{
426+
"command": "coder.tasks.refresh",
427+
"when": "coder.authenticated && view == coder.tasksPanel",
428+
"group": "navigation@1"
407429
}
408430
],
409431
"view/item/context": [
@@ -478,11 +500,12 @@
478500
"@types/ws": "^8.18.1",
479501
"@typescript-eslint/eslint-plugin": "^8.53.1",
480502
"@typescript-eslint/parser": "^8.53.1",
481-
"@vitejs/plugin-react-swc": "catalog:",
503+
"@vitejs/plugin-react": "catalog:",
482504
"@vitest/coverage-v8": "^4.0.16",
483505
"@vscode/test-cli": "^0.0.12",
484506
"@vscode/test-electron": "^2.5.2",
485507
"@vscode/vsce": "^3.7.1",
508+
"babel-plugin-react-compiler": "catalog:",
486509
"bufferutil": "^4.1.0",
487510
"coder": "github:coder/coder#main",
488511
"concurrently": "^9.2.1",
@@ -495,6 +518,7 @@
495518
"eslint-plugin-import-x": "^4.16.1",
496519
"eslint-plugin-package-json": "^0.88.2",
497520
"eslint-plugin-react": "^7.37.0",
521+
"eslint-plugin-react-compiler": "catalog:",
498522
"eslint-plugin-react-hooks": "^5.0.0",
499523
"globals": "^17.0.0",
500524
"jsdom": "^27.4.0",

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: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,61 @@
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</h3>
57+
<p>Templates: {templates.length}</p>
58+
<p>Tasks: {tasks.length}</p>
3959
</div>
4060
);
4161
}

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: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ 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(
18+
message: WebviewMessage | Record<string, unknown>,
19+
): void {
1420
getVsCodeApi().postMessage(message);
1521
}
1622

0 commit comments

Comments
 (0)