Skip to content

Commit c2e8aaa

Browse files
authored
Add Tasks panel infrastructure with type-safe IPC protocol (#772)
Build the communication and data-fetching infrastructure for the Tasks webview panel, gated behind `coder.experimental.tasks` (disabled by default). Includes shared IPC types, extension backend, React hooks, and build tooling changes. - Add `@repo/shared` package with `defineRequest`/`defineCommand`/ `defineNotification` factories using phantom types for compile-time message safety - Add `TasksApi` namespace defining all messages (init, CRUD, pause/resume, viewInCoder, logs, notifications) - Add `TasksPanel` with typed handler dispatch, template caching (5min TTL), log caching for stable states, and 404-based feature detection for servers without Tasks support - Add `useIpc()` hook for request correlation, 30s timeouts, and notification subscriptions - Add `useTasksApi()` typed wrapper over all Tasks operations with React Query integration - Add `coder.experimental.tasks` boolean setting and `coder.tasksEnabled` context variable so the sidebar only appears when opted in - Switch to `@vitejs/plugin-react` with `babel-plugin-react-compiler` and `eslint-plugin-react-compiler` - Add `@tanstack/react-query`, `@vscode/codicons` dependencies - Add codicon font handling for vscode-elements shadow DOM
1 parent f47af40 commit c2e8aaa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2570
-320
lines changed

esbuild.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ 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",
3434
},
3535
external: ["vscode"],

eslint.config.mjs

Lines changed: 3 additions & 1 deletion
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: {
@@ -189,7 +191,7 @@ export default defineConfig(
189191
},
190192
},
191193
rules: {
192-
// TS rules already applied above; add React-specific rules
194+
...reactCompilerPlugin.configs.recommended.rules,
193195
...reactPlugin.configs.recommended.rules,
194196
...reactPlugin.configs["jsx-runtime"].rules, // React 17+ JSX transform
195197
...reactHooksPlugin.configs.recommended.rules,

media/tasks-logo.svg

Lines changed: 4 additions & 0 deletions
Loading

package.json

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@
140140
"experimental"
141141
]
142142
},
143+
"coder.experimental.tasks": {
144+
"markdownDescription": "Enable the experimental [Tasks](https://coder.com/docs/ai-coder/tasks) panel in VS Code. When enabled, a sidebar panel lets you run and manage AI coding agents in Coder workspaces. This feature is under active development and may change. Requires a Coder deployment with Tasks support.",
145+
"type": "boolean",
146+
"default": false,
147+
"tags": [
148+
"experimental"
149+
]
150+
},
143151
"coder.sshFlags": {
144152
"markdownDescription": "Additional flags to pass to the `coder ssh` command when establishing SSH connections. Enter each flag as a separate array item; values are passed verbatim and in order. See the [CLI ssh reference](https://coder.com/docs/reference/cli/ssh) for available flags.\n\nNote: `--network-info-dir` and `--ssh-host-prefix` are ignored (managed internally). Prefer `#coder.proxyLogDirectory#` over `--log-dir`/`-l` for full functionality.",
145153
"type": "array",
@@ -182,6 +190,11 @@
182190
"id": "coder",
183191
"title": "Coder Remote",
184192
"icon": "media/logo-white.svg"
193+
},
194+
{
195+
"id": "coderTasks",
196+
"title": "Coder Tasks",
197+
"icon": "media/tasks-logo.svg"
185198
}
186199
]
187200
},
@@ -199,13 +212,15 @@
199212
"visibility": "visible",
200213
"icon": "media/logo-white.svg",
201214
"when": "coder.authenticated && coder.isOwner"
202-
},
215+
}
216+
],
217+
"coderTasks": [
203218
{
204219
"type": "webview",
205220
"id": "coder.tasksPanel",
206-
"name": "Tasks",
207-
"icon": "media/logo-white.svg",
208-
"when": "coder.authenticated && coder.devMode"
221+
"name": "Coder Tasks",
222+
"icon": "media/tasks-logo.svg",
223+
"when": "coder.authenticated && coder.tasksEnabled"
209224
}
210225
]
211226
},
@@ -218,7 +233,7 @@
218233
{
219234
"view": "coder.tasksPanel",
220235
"contents": "[Login](command:coder.login) to view tasks.",
221-
"when": "!coder.authenticated && coder.loaded"
236+
"when": "!coder.authenticated && coder.loaded && coder.tasksEnabled"
222237
}
223238
],
224239
"commands": [
@@ -308,6 +323,12 @@
308323
"command": "coder.manageCredentials",
309324
"title": "Manage Credentials",
310325
"category": "Coder"
326+
},
327+
{
328+
"command": "coder.tasks.refresh",
329+
"title": "Refresh Tasks",
330+
"category": "Coder",
331+
"icon": "$(refresh)"
311332
}
312333
],
313334
"menus": {
@@ -370,6 +391,10 @@
370391
},
371392
{
372393
"command": "coder.manageCredentials"
394+
},
395+
{
396+
"command": "coder.tasks.refresh",
397+
"when": "false"
373398
}
374399
],
375400
"view/title": [
@@ -404,6 +429,11 @@
404429
"command": "coder.searchAllWorkspaces",
405430
"when": "coder.authenticated && view == allWorkspaces",
406431
"group": "navigation@3"
432+
},
433+
{
434+
"command": "coder.tasks.refresh",
435+
"when": "coder.authenticated && coder.tasksEnabled && view == coder.tasksPanel",
436+
"group": "navigation@1"
407437
}
408438
],
409439
"view/item/context": [
@@ -448,6 +478,7 @@
448478
},
449479
"dependencies": {
450480
"@peculiar/x509": "^1.14.3",
481+
"@repo/shared": "workspace:*",
451482
"axios": "1.13.5",
452483
"date-fns": "^4.1.0",
453484
"eventsource": "^4.1.0",
@@ -478,11 +509,12 @@
478509
"@types/ws": "^8.18.1",
479510
"@typescript-eslint/eslint-plugin": "^8.54.0",
480511
"@typescript-eslint/parser": "^8.54.0",
481-
"@vitejs/plugin-react-swc": "catalog:",
512+
"@vitejs/plugin-react": "catalog:",
482513
"@vitest/coverage-v8": "^4.0.18",
483514
"@vscode/test-cli": "^0.0.12",
484515
"@vscode/test-electron": "^2.5.2",
485516
"@vscode/vsce": "^3.7.1",
517+
"babel-plugin-react-compiler": "catalog:",
486518
"bufferutil": "^4.1.0",
487519
"coder": "github:coder/coder#main",
488520
"concurrently": "^9.2.1",
@@ -495,6 +527,7 @@
495527
"eslint-plugin-import-x": "^4.16.1",
496528
"eslint-plugin-package-json": "^0.88.2",
497529
"eslint-plugin-react": "^7.37.5",
530+
"eslint-plugin-react-compiler": "catalog:",
498531
"eslint-plugin-react-hooks": "^7.0.1",
499532
"globals": "^17.3.0",
500533
"jsdom": "^28.0.0",

packages/shared/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "@repo/shared",
3+
"version": "1.0.0",
4+
"description": "Shared types and utilities for extension and webviews",
5+
"private": true,
6+
"type": "module",
7+
"exports": {
8+
".": {
9+
"types": "./src/index.ts",
10+
"default": "./src/index.ts"
11+
}
12+
},
13+
"devDependencies": {
14+
"typescript": "catalog:"
15+
}
16+
}

packages/shared/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// IPC protocol types
2+
export * from "./ipc/protocol";
3+
4+
// Tasks types and API
5+
export * from "./tasks/types";
6+
export * from "./tasks/api";
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Type-safe IPC protocol for VS Code webview communication.
3+
*
4+
* Inspired by tRPC's approach: types are carried in a phantom `_types` property
5+
* that exists only for TypeScript inference, not at runtime.
6+
*/
7+
8+
// --- Message definitions ---
9+
10+
/** Request definition: params P, response R */
11+
export interface RequestDef<P = void, R = void> {
12+
readonly method: string;
13+
/** @internal Phantom types for inference - not present at runtime */
14+
readonly _types?: { params: P; response: R };
15+
}
16+
17+
/** Command definition: params P, no response */
18+
export interface CommandDef<P = void> {
19+
readonly method: string;
20+
/** @internal Phantom type for inference - not present at runtime */
21+
readonly _types?: { params: P };
22+
}
23+
24+
/** Notification definition: data D (extension to webview) */
25+
export interface NotificationDef<D = void> {
26+
readonly method: string;
27+
/** @internal Phantom type for inference - not present at runtime */
28+
readonly _types?: { data: D };
29+
}
30+
31+
// --- Factory functions ---
32+
33+
/** Define a request with typed params and response */
34+
export function defineRequest<P = void, R = void>(
35+
method: string,
36+
): RequestDef<P, R> {
37+
return { method } as RequestDef<P, R>;
38+
}
39+
40+
/** Define a fire-and-forget command */
41+
export function defineCommand<P = void>(method: string): CommandDef<P> {
42+
return { method } as CommandDef<P>;
43+
}
44+
45+
/** Define a push notification (extension to webview) */
46+
export function defineNotification<D = void>(
47+
method: string,
48+
): NotificationDef<D> {
49+
return { method } as NotificationDef<D>;
50+
}
51+
52+
// --- Wire format ---
53+
54+
/** Request from webview to extension */
55+
export interface IpcRequest<P = unknown> {
56+
readonly requestId: string;
57+
readonly method: string;
58+
readonly params?: P;
59+
}
60+
61+
/** Response from extension to webview */
62+
export interface IpcResponse<T = unknown> {
63+
readonly requestId: string;
64+
readonly method: string;
65+
readonly success: boolean;
66+
readonly data?: T;
67+
readonly error?: string;
68+
}
69+
70+
/** Push notification from extension to webview */
71+
export interface IpcNotification<D = unknown> {
72+
readonly type: string;
73+
readonly data?: D;
74+
}
75+
76+
// --- Handler utilities ---
77+
78+
/** Extract params type from a request/command definition */
79+
export type ParamsOf<T> = T extends { _types?: { params: infer P } } ? P : void;
80+
81+
/** Extract response type from a request definition */
82+
export type ResponseOf<T> = T extends { _types?: { response: infer R } }
83+
? R
84+
: void;
85+
86+
/** Type-safe request handler - infers params and return type from definition */
87+
export function requestHandler<P, R>(
88+
_def: RequestDef<P, R>,
89+
fn: (params: P) => Promise<R>,
90+
): (params: unknown) => Promise<unknown> {
91+
return fn as (params: unknown) => Promise<unknown>;
92+
}
93+
94+
/** Type-safe command handler - infers params type from definition */
95+
export function commandHandler<P>(
96+
_def: CommandDef<P>,
97+
fn: (params: P) => void | Promise<void>,
98+
): (params: unknown) => void | Promise<void> {
99+
return fn as (params: unknown) => void | Promise<void>;
100+
}

packages/shared/src/tasks/api.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Tasks API - Type-safe message definitions for the Tasks webview.
3+
*
4+
* Usage:
5+
* ```tsx
6+
* const ipc = useIpc();
7+
* const tasks = await ipc.request(TasksApi.getTasks); // Returns Task[]
8+
* ipc.command(TasksApi.viewInCoder, { taskId: "..." }); // Fire-and-forget
9+
* ```
10+
*/
11+
12+
import {
13+
defineCommand,
14+
defineNotification,
15+
defineRequest,
16+
} from "../ipc/protocol";
17+
18+
import type { Task, TaskDetails, TaskLogEntry, TaskTemplate } from "./types";
19+
20+
export interface InitResponse {
21+
tasks: readonly Task[];
22+
templates: readonly TaskTemplate[];
23+
baseUrl: string;
24+
tasksSupported: boolean;
25+
}
26+
27+
const init = defineRequest<void, InitResponse>("init");
28+
const getTasks = defineRequest<void, Task[]>("getTasks");
29+
const getTemplates = defineRequest<void, TaskTemplate[]>("getTemplates");
30+
const getTask = defineRequest<{ taskId: string }, Task>("getTask");
31+
const getTaskDetails = defineRequest<{ taskId: string }, TaskDetails>(
32+
"getTaskDetails",
33+
);
34+
35+
export interface CreateTaskParams {
36+
templateVersionId: string;
37+
prompt: string;
38+
presetId?: string;
39+
}
40+
const createTask = defineRequest<CreateTaskParams, Task>("createTask");
41+
42+
const deleteTask = defineRequest<{ taskId: string }, void>("deleteTask");
43+
const pauseTask = defineRequest<{ taskId: string }, void>("pauseTask");
44+
const resumeTask = defineRequest<{ taskId: string }, void>("resumeTask");
45+
46+
const viewInCoder = defineCommand<{ taskId: string }>("viewInCoder");
47+
const viewLogs = defineCommand<{ taskId: string }>("viewLogs");
48+
const downloadLogs = defineCommand<{ taskId: string }>("downloadLogs");
49+
const sendTaskMessage = defineCommand<{
50+
taskId: string;
51+
message: string;
52+
}>("sendTaskMessage");
53+
54+
const taskUpdated = defineNotification<Task>("taskUpdated");
55+
const tasksUpdated = defineNotification<Task[]>("tasksUpdated");
56+
const logsAppend = defineNotification<TaskLogEntry[]>("logsAppend");
57+
const refresh = defineNotification<void>("refresh");
58+
const showCreateForm = defineNotification<void>("showCreateForm");
59+
60+
export const TasksApi = {
61+
// Requests
62+
init,
63+
getTasks,
64+
getTemplates,
65+
getTask,
66+
getTaskDetails,
67+
createTask,
68+
deleteTask,
69+
pauseTask,
70+
resumeTask,
71+
// Commands
72+
viewInCoder,
73+
viewLogs,
74+
downloadLogs,
75+
sendTaskMessage,
76+
// Notifications
77+
taskUpdated,
78+
tasksUpdated,
79+
logsAppend,
80+
refresh,
81+
showCreateForm,
82+
} as const;

0 commit comments

Comments
 (0)