Skip to content

Commit 4fdc74e

Browse files
committed
Self-review
1 parent 360d305 commit 4fdc74e

41 files changed

Lines changed: 1115 additions & 1354 deletions

Some content is hidden

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

esbuild.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ const buildOptions = {
3131
// Force openpgp to use CJS. The ESM version uses import.meta.url which is
3232
// 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",
3534
},
3635
external: ["vscode"],
3736
sourcemap: production ? "external" : true,

eslint.config.mjs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,11 @@ export default defineConfig(
191191
},
192192
},
193193
rules: {
194-
// TS rules already applied above; add React-specific rules
194+
...reactCompilerPlugin.configs.recommended.rules,
195195
...reactPlugin.configs.recommended.rules,
196196
...reactPlugin.configs["jsx-runtime"].rules, // React 17+ JSX transform
197197
...reactHooksPlugin.configs.recommended.rules,
198198
"react/prop-types": "off", // Using TypeScript
199-
"react-compiler/react-compiler": "error",
200199
},
201200
},
202201

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@
470470
},
471471
"dependencies": {
472472
"@peculiar/x509": "^1.14.3",
473+
"@repo/shared": "workspace:*",
473474
"axios": "1.13.4",
474475
"date-fns": "^4.1.0",
475476
"eventsource": "^4.1.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: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
readonly scope?: string;
14+
/** @internal Phantom types for inference - not present at runtime */
15+
readonly _types?: { params: P; response: R };
16+
}
17+
18+
/** Command definition: params P, no response */
19+
export interface CommandDef<P = void> {
20+
readonly method: string;
21+
readonly scope?: string;
22+
/** @internal Phantom type for inference - not present at runtime */
23+
readonly _types?: { params: P };
24+
}
25+
26+
/** Notification definition: data D (extension to webview) */
27+
export interface NotificationDef<D = void> {
28+
readonly method: string;
29+
readonly scope?: string;
30+
/** @internal Phantom type for inference - not present at runtime */
31+
readonly _types?: { data: D };
32+
}
33+
34+
// --- Factory functions ---
35+
36+
/** Define a request with typed params and response */
37+
export function defineRequest<P = void, R = void>(
38+
method: string,
39+
scope?: string,
40+
): RequestDef<P, R> {
41+
return { method, scope } as RequestDef<P, R>;
42+
}
43+
44+
/** Define a fire-and-forget command */
45+
export function defineCommand<P = void>(
46+
method: string,
47+
scope?: string,
48+
): CommandDef<P> {
49+
return { method, scope } as CommandDef<P>;
50+
}
51+
52+
/** Define a push notification (extension to webview) */
53+
export function defineNotification<D = void>(
54+
method: string,
55+
scope?: string,
56+
): NotificationDef<D> {
57+
return { method, scope } as NotificationDef<D>;
58+
}
59+
60+
// --- Wire format ---
61+
62+
/** Request from webview to extension */
63+
export interface IpcRequest<P = unknown> {
64+
readonly requestId: string;
65+
readonly method: string;
66+
readonly scope?: string;
67+
readonly params?: P;
68+
}
69+
70+
/** Response from extension to webview */
71+
export interface IpcResponse<T = unknown> {
72+
readonly requestId: string;
73+
readonly method: string;
74+
readonly success: boolean;
75+
readonly data?: T;
76+
readonly error?: string;
77+
}
78+
79+
/** Push notification from extension to webview */
80+
export interface IpcNotification<D = unknown> {
81+
readonly type: string;
82+
readonly data?: D;
83+
}
84+
85+
// --- Handler utilities ---
86+
87+
/** Extract params type from a request/command definition */
88+
export type ParamsOf<T> = T extends { _types?: { params: infer P } } ? P : void;
89+
90+
/** Extract response type from a request definition */
91+
export type ResponseOf<T> = T extends { _types?: { response: infer R } }
92+
? R
93+
: void;
94+
95+
/** Type-safe request handler - infers params and return type from definition */
96+
export function requestHandler<P, R>(
97+
_def: RequestDef<P, R>,
98+
fn: (params: P) => Promise<R>,
99+
): (params: unknown) => Promise<unknown> {
100+
return fn as (params: unknown) => Promise<unknown>;
101+
}
102+
103+
/** Type-safe command handler - infers params type from definition */
104+
export function commandHandler<P>(
105+
_def: CommandDef<P>,
106+
fn: (params: P) => void | Promise<void>,
107+
): (params: unknown) => void | Promise<void> {
108+
return fn as (params: unknown) => void | Promise<void>;
109+
}
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ import {
1717

1818
import type { Task, TaskDetails, TaskLogEntry, TaskTemplate } from "./types";
1919

20-
// =============================================================================
21-
// Requests (expect response)
22-
// =============================================================================
20+
// --- Requests ---
2321

2422
export interface InitResponse {
2523
tasks: Task[];
@@ -47,9 +45,7 @@ export const deleteTask = defineRequest<{ taskId: string }, void>("deleteTask");
4745
export const pauseTask = defineRequest<{ taskId: string }, void>("pauseTask");
4846
export const resumeTask = defineRequest<{ taskId: string }, void>("resumeTask");
4947

50-
// =============================================================================
51-
// Commands (fire-and-forget)
52-
// =============================================================================
48+
// --- Commands ---
5349

5450
export const viewInCoder = defineCommand<{ taskId: string }>("viewInCoder");
5551
export const viewLogs = defineCommand<{ taskId: string }>("viewLogs");
@@ -59,19 +55,15 @@ export const sendTaskMessage = defineCommand<{
5955
message: string;
6056
}>("sendTaskMessage");
6157

62-
// =============================================================================
63-
// Notifications (extension → webview push)
64-
// =============================================================================
58+
// --- Notifications ---
6559

6660
export const taskUpdated = defineNotification<Task>("taskUpdated");
6761
export const tasksUpdated = defineNotification<Task[]>("tasksUpdated");
6862
export const logsAppend = defineNotification<TaskLogEntry[]>("logsAppend");
6963
export const refresh = defineNotification<void>("refresh");
7064
export const showCreateForm = defineNotification<void>("showCreateForm");
7165

72-
// =============================================================================
73-
// Grouped export
74-
// =============================================================================
66+
// --- Grouped export ---
7567

7668
export const TasksApi = {
7769
// Requests

packages/webview-shared/src/tasks/types.ts renamed to packages/shared/src/tasks/types.ts

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,29 @@ export interface TaskActions {
5757
canResume: boolean;
5858
}
5959

60+
const RESUMABLE_STATUSES: readonly WorkspaceStatus[] = [
61+
"stopped",
62+
"failed",
63+
"canceled",
64+
];
65+
66+
const PAUSED_STATUSES: readonly WorkspaceStatus[] = [
67+
"stopped",
68+
"stopping",
69+
"canceled",
70+
];
71+
72+
const INITIALIZING_STATUSES: readonly WorkspaceStatus[] = [
73+
"starting",
74+
"pending",
75+
];
76+
6077
export function getTaskActions(task: Task): TaskActions {
6178
const hasWorkspace = task.workspace_id !== null;
79+
const status = task.workspace_status;
6280
return {
63-
canPause: hasWorkspace && task.workspace_status === "running",
64-
canResume:
65-
hasWorkspace &&
66-
(task.workspace_status === "stopped" ||
67-
task.workspace_status === "failed" ||
68-
task.workspace_status === "canceled"),
81+
canPause: hasWorkspace && status === "running",
82+
canResume: hasWorkspace && !!status && RESUMABLE_STATUSES.includes(status),
6983
};
7084
}
7185

@@ -81,36 +95,27 @@ export type TaskUIState =
8195
/** Compute the UI state from a Task object */
8296
export function getTaskUIState(task: Task): TaskUIState {
8397
const taskState = task.current_state?.state;
98+
const workspaceStatus = task.workspace_status;
8499

85-
// Error takes priority
86100
if (task.status === "error" || taskState === "failed") {
87101
return "error";
88102
}
89103

90-
// Check workspace status for paused/initializing
91-
if (
92-
task.workspace_status === "stopped" ||
93-
task.workspace_status === "stopping" ||
94-
task.workspace_status === "canceled"
95-
) {
104+
if (workspaceStatus && PAUSED_STATUSES.includes(workspaceStatus)) {
96105
return "paused";
97106
}
98-
if (
99-
task.workspace_status === "starting" ||
100-
task.workspace_status === "pending"
101-
) {
107+
108+
if (workspaceStatus && INITIALIZING_STATUSES.includes(workspaceStatus)) {
102109
return "initializing";
103110
}
104111

105-
// Active task states
106-
if (task.status === "active" && task.workspace_status === "running") {
107-
if (taskState === "working") return "working";
108-
if (taskState === "idle") return "idle";
109-
if (taskState === "complete") return "complete";
112+
if (task.status === "active" && workspaceStatus === "running" && taskState) {
113+
return taskState;
110114
}
111115

112-
// Completed without running workspace
113-
if (taskState === "complete") return "complete";
116+
if (taskState === "complete") {
117+
return "complete";
118+
}
114119

115-
return "idle"; // default fallback
120+
return "idle";
116121
}

packages/shared/tsconfig.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../tsconfig.packages.json",
3+
"compilerOptions": {
4+
"composite": true,
5+
"declaration": true,
6+
"noEmit": false,
7+
"outDir": "dist",
8+
"rootDir": "src",
9+
"tsBuildInfoFile": "dist/.tsbuildinfo"
10+
},
11+
"include": ["src"]
12+
}

packages/tasks/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"dev": "vite build --watch"
1010
},
1111
"dependencies": {
12+
"@repo/shared": "workspace:*",
1213
"@repo/webview-shared": "workspace:*",
14+
"@tanstack/react-query": "catalog:",
1315
"@vscode-elements/react-elements": "catalog:",
1416
"@vscode/codicons": "catalog:",
1517
"react": "catalog:",

0 commit comments

Comments
 (0)