Skip to content

Commit d241239

Browse files
committed
Add mapped-type handler builders and kind discriminants to IPC protocol
Replace per-definition requestHandler/commandHandler wrappers with buildRequestHandlers and buildCommandHandlers that enforce compile-time completeness over the full API surface. - Add kind discriminant to RequestDef, CommandDef, and NotificationDef interfaces and their factory functions - Add RequestHandlerMap and CommandHandlerMap mapped types that require a handler for every definition of the corresponding kind - Add buildRequestHandlers and buildCommandHandlers builder functions with typed overloads for compile-time completeness checking - Replace manual method-keyed handler maps in tasksPanelProvider with builder calls using property-name keys - Replace raw sendNotification calls with a typed notify helper
1 parent 1d32675 commit d241239

File tree

2 files changed

+98
-99
lines changed

2 files changed

+98
-99
lines changed

packages/shared/src/ipc/protocol.ts

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,23 @@
99

1010
/** Request definition: params P, response R */
1111
export interface RequestDef<P = void, R = void> {
12+
readonly kind: "request";
1213
readonly method: string;
1314
/** @internal Phantom types for inference - not present at runtime */
1415
readonly _types?: { params: P; response: R };
1516
}
1617

1718
/** Command definition: params P, no response */
1819
export interface CommandDef<P = void> {
20+
readonly kind: "command";
1921
readonly method: string;
2022
/** @internal Phantom type for inference - not present at runtime */
2123
readonly _types?: { params: P };
2224
}
2325

2426
/** Notification definition: data D (extension to webview) */
2527
export interface NotificationDef<D = void> {
28+
readonly kind: "notification";
2629
readonly method: string;
2730
/** @internal Phantom type for inference - not present at runtime */
2831
readonly _types?: { data: D };
@@ -34,19 +37,19 @@ export interface NotificationDef<D = void> {
3437
export function defineRequest<P = void, R = void>(
3538
method: string,
3639
): RequestDef<P, R> {
37-
return { method } as RequestDef<P, R>;
40+
return { kind: "request", method } as RequestDef<P, R>;
3841
}
3942

4043
/** Define a fire-and-forget command */
4144
export function defineCommand<P = void>(method: string): CommandDef<P> {
42-
return { method } as CommandDef<P>;
45+
return { kind: "command", method } as CommandDef<P>;
4346
}
4447

4548
/** Define a push notification (extension to webview) */
4649
export function defineNotification<D = void>(
4750
method: string,
4851
): NotificationDef<D> {
49-
return { method } as NotificationDef<D>;
52+
return { kind: "notification", method } as NotificationDef<D>;
5053
}
5154

5255
// --- Wire format ---
@@ -83,18 +86,60 @@ export type ResponseOf<T> = T extends { _types?: { response: infer R } }
8386
? R
8487
: void;
8588

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>;
89+
// --- Mapped types for handler completeness ---
90+
91+
/** Requires a handler for every RequestDef in Api. Compile error if one is missing. */
92+
export type RequestHandlerMap<Api> = {
93+
[K in keyof Api as Api[K] extends { kind: "request" }
94+
? K
95+
: never]: Api[K] extends RequestDef<infer P, infer R>
96+
? (params: P) => Promise<R>
97+
: never;
98+
};
99+
100+
/** Requires a handler for every CommandDef in Api. Compile error if one is missing. */
101+
export type CommandHandlerMap<Api> = {
102+
[K in keyof Api as Api[K] extends { kind: "command" }
103+
? K
104+
: never]: Api[K] extends CommandDef<infer P>
105+
? (params: P) => void | Promise<void>
106+
: never;
107+
};
108+
109+
// --- Builder functions ---
110+
111+
/** Build a method-indexed map of request handlers with compile-time completeness. */
112+
export function buildRequestHandlers<
113+
Api extends Record<string, { method: string }>,
114+
>(
115+
api: Api,
116+
handlers: RequestHandlerMap<Api>,
117+
): Record<string, (params: unknown) => Promise<unknown>>;
118+
export function buildRequestHandlers(
119+
api: Record<string, { method: string }>,
120+
handlers: Record<string, (params: unknown) => Promise<unknown>>,
121+
) {
122+
const result: Record<string, (params: unknown) => Promise<unknown>> = {};
123+
for (const key of Object.keys(handlers)) {
124+
result[api[key].method] = handlers[key];
125+
}
126+
return result;
92127
}
93128

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>;
129+
/** Build a method-indexed map of command handlers with compile-time completeness. */
130+
export function buildCommandHandlers<
131+
Api extends Record<string, { method: string }>,
132+
>(
133+
api: Api,
134+
handlers: CommandHandlerMap<Api>,
135+
): Record<string, (params: unknown) => void | Promise<void>>;
136+
export function buildCommandHandlers(
137+
api: Record<string, { method: string }>,
138+
handlers: Record<string, (params: unknown) => void | Promise<void>>,
139+
) {
140+
const result: Record<string, (params: unknown) => void | Promise<void>> = {};
141+
for (const key of Object.keys(handlers)) {
142+
result[api[key].method] = handlers[key];
143+
}
144+
return result;
100145
}

src/webviews/tasks/tasksPanelProvider.ts

Lines changed: 38 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import stripAnsi from "strip-ansi";
33
import * as vscode from "vscode";
44

55
import {
6-
commandHandler,
6+
buildCommandHandlers,
7+
buildRequestHandlers,
78
isBuildingWorkspace,
89
isAgentStarting,
910
getTaskPermissions,
1011
getTaskLabel,
1112
isStableTask,
12-
requestHandler,
1313
TasksApi,
1414
type CreateTaskParams,
15-
type IpcNotification,
1615
type IpcRequest,
1716
type IpcResponse,
17+
type NotificationDef,
1818
type TaskDetails,
1919
type TaskLogs,
2020
type TaskTemplate,
@@ -100,71 +100,28 @@ export class TasksPanelProvider
100100
logs: TaskLogs;
101101
};
102102

103-
/**
104-
* Request handlers indexed by method name.
105-
* Type safety is ensured at definition time via requestHandler().
106-
*/
107-
private readonly requestHandlers: Record<
108-
string,
109-
(params: unknown) => Promise<unknown>
110-
> = {
111-
[TasksApi.getTasks.method]: requestHandler(TasksApi.getTasks, () =>
112-
this.fetchTasks(),
113-
),
114-
[TasksApi.getTemplates.method]: requestHandler(TasksApi.getTemplates, () =>
115-
this.fetchTemplates(),
116-
),
117-
[TasksApi.getTask.method]: requestHandler(TasksApi.getTask, (p) =>
118-
this.client.getTask("me", p.taskId),
119-
),
120-
[TasksApi.getTaskDetails.method]: requestHandler(
121-
TasksApi.getTaskDetails,
122-
(p) => this.handleGetTaskDetails(p.taskId),
123-
),
124-
[TasksApi.createTask.method]: requestHandler(TasksApi.createTask, (p) =>
125-
this.handleCreateTask(p),
126-
),
127-
[TasksApi.deleteTask.method]: requestHandler(TasksApi.deleteTask, (p) =>
128-
this.handleDeleteTask(p.taskId, p.taskName),
129-
),
130-
[TasksApi.pauseTask.method]: requestHandler(TasksApi.pauseTask, (p) =>
131-
this.handlePauseTask(p.taskId, p.taskName),
132-
),
133-
[TasksApi.resumeTask.method]: requestHandler(TasksApi.resumeTask, (p) =>
134-
this.handleResumeTask(p.taskId, p.taskName),
135-
),
136-
[TasksApi.downloadLogs.method]: requestHandler(TasksApi.downloadLogs, (p) =>
137-
this.handleDownloadLogs(p.taskId),
138-
),
139-
[TasksApi.sendTaskMessage.method]: requestHandler(
140-
TasksApi.sendTaskMessage,
141-
(p) => this.handleSendMessage(p.taskId, p.message),
142-
),
143-
};
144-
145-
/**
146-
* Command handlers indexed by method name.
147-
* Type safety is ensured at definition time via commandHandler().
148-
*/
149-
private readonly commandHandlers: Record<
150-
string,
151-
(params: unknown) => void | Promise<void>
152-
> = {
153-
[TasksApi.viewInCoder.method]: commandHandler(TasksApi.viewInCoder, (p) =>
154-
this.handleViewInCoder(p.taskId),
155-
),
156-
[TasksApi.viewLogs.method]: commandHandler(TasksApi.viewLogs, (p) =>
157-
this.handleViewLogs(p.taskId),
158-
),
159-
[TasksApi.stopStreamingWorkspaceLogs.method]: commandHandler(
160-
TasksApi.stopStreamingWorkspaceLogs,
161-
() => {
162-
this.streamingTaskId = null;
163-
this.buildLogStream.close();
164-
this.agentLogStream.close();
165-
},
166-
),
167-
};
103+
private readonly requestHandlers = buildRequestHandlers(TasksApi, {
104+
getTasks: () => this.fetchTasks(),
105+
getTemplates: () => this.fetchTemplates(),
106+
getTask: (p) => this.client.getTask("me", p.taskId),
107+
getTaskDetails: (p) => this.handleGetTaskDetails(p.taskId),
108+
createTask: (p) => this.handleCreateTask(p),
109+
deleteTask: (p) => this.handleDeleteTask(p.taskId, p.taskName),
110+
pauseTask: (p) => this.handlePauseTask(p.taskId, p.taskName),
111+
resumeTask: (p) => this.handleResumeTask(p.taskId, p.taskName),
112+
downloadLogs: (p) => this.handleDownloadLogs(p.taskId),
113+
sendTaskMessage: (p) => this.handleSendMessage(p.taskId, p.message),
114+
});
115+
116+
private readonly commandHandlers = buildCommandHandlers(TasksApi, {
117+
viewInCoder: (p) => this.handleViewInCoder(p.taskId),
118+
viewLogs: (p) => this.handleViewLogs(p.taskId),
119+
stopStreamingWorkspaceLogs: () => {
120+
this.streamingTaskId = null;
121+
this.buildLogStream.close();
122+
this.agentLogStream.close();
123+
},
124+
});
168125

169126
constructor(
170127
private readonly extensionUri: vscode.Uri,
@@ -173,12 +130,12 @@ export class TasksPanelProvider
173130
) {}
174131

175132
public showCreateForm(): void {
176-
this.sendNotification({ type: TasksApi.showCreateForm.method });
133+
this.notify(TasksApi.showCreateForm);
177134
}
178135

179136
public refresh(): void {
180137
this.cachedLogs = undefined;
181-
this.sendNotification({ type: TasksApi.refresh.method });
138+
this.notify(TasksApi.refresh);
182139
}
183140

184141
resolveWebviewView(
@@ -452,10 +409,7 @@ export class TasksPanelProvider
452409
const clean = stripAnsi(line);
453410
// Skip lines that were purely ANSI codes, but keep intentional blank lines.
454411
if (line.length > 0 && clean.length === 0) return;
455-
this.sendNotification({
456-
type: TasksApi.workspaceLogsAppend.method,
457-
data: [clean],
458-
});
412+
this.notify(TasksApi.workspaceLogsAppend, [clean]);
459413
};
460414

461415
const onStreamClose = () => {
@@ -514,10 +468,7 @@ export class TasksPanelProvider
514468
try {
515469
const tasks = await this.fetchTasks();
516470
if (tasks !== null) {
517-
this.sendNotification({
518-
type: TasksApi.tasksUpdated.method,
519-
data: tasks,
520-
});
471+
this.notify(TasksApi.tasksUpdated, [...tasks]);
521472
}
522473
} catch (err) {
523474
this.logger.warn("Failed to refresh tasks after action", err);
@@ -527,10 +478,7 @@ export class TasksPanelProvider
527478
private async refreshAndNotifyTask(taskId: string): Promise<void> {
528479
try {
529480
const task = await this.client.getTask("me", taskId);
530-
this.sendNotification({
531-
type: TasksApi.taskUpdated.method,
532-
data: task,
533-
});
481+
this.notify(TasksApi.taskUpdated, task);
534482
} catch (err) {
535483
this.logger.warn("Failed to refresh task after action", err);
536484
}
@@ -616,8 +564,14 @@ export class TasksPanelProvider
616564
this.view?.webview.postMessage(response);
617565
}
618566

619-
private sendNotification(notification: IpcNotification): void {
620-
this.view?.webview.postMessage(notification);
567+
private notify<D>(
568+
def: NotificationDef<D>,
569+
...args: D extends void ? [] : [data: D]
570+
): void {
571+
this.view?.webview.postMessage({
572+
type: def.method,
573+
...(args.length > 0 ? { data: args[0] } : {}),
574+
});
621575
}
622576

623577
dispose(): void {

0 commit comments

Comments
 (0)