豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit f59e46d

Browse files
feat: wire hooks through CopilotCLI customization provider (#4952)
* feat: wire hooks through CopilotCLI customization provider - Mirror updated chatPromptFiles d.ts with ChatHookProvider - Add hooks/onDidChangeHooks to IChatPromptFileService interface - Wire vscode.chat.hooks in ChatPromptFileService implementation - Add Hook to CopilotCLI provider supportedTypes (unhides section) - Add getHookItems() to CopilotCLI provider - Subscribe to onDidChangeHooks for provider invalidation - Fix all test mocks for new interface member * chore: update vscodeCommit pointer via vscode-dts:update * chore: remove backward compat guards for hooks API --------- Co-authored-by: Justin Chen <54879025+justschen@users.noreply.github.com>
1 parent 1d26378 commit f59e46d

File tree

9 files changed

+140
-5
lines changed

9 files changed

+140
-5
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6400,5 +6400,5 @@
64006400
"node-gyp": "npm:node-gyp@10.3.1",
64016401
"zod": "3.25.76"
64026402
},
6403-
"vscodeCommit": "f540be8c1abf14ef594d42dd3a99c50f48732a7b"
6403+
"vscodeCommit": "d0c21ae98319ccc4f796b6557b945faa9b72a3af"
64046404
}

src/extension/chatSessions/common/chatPromptFileService.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,16 @@ export interface IChatPromptFileService extends IDisposable {
5050
* from all sources (workspace, user, and extension-provided).
5151
*/
5252
readonly skills: readonly ChatResource[];
53+
54+
/**
55+
* An event that fires when the list of {@link hooks hooks} changes.
56+
*/
57+
readonly onDidChangeHooks: Event<void>;
58+
59+
/**
60+
* The list of currently available hook configuration files.
61+
* These are JSON files that define lifecycle hooks from all sources
62+
* (workspace, user, and extension-provided).
63+
*/
64+
readonly hooks: readonly ChatResource[];
5365
}

src/extension/chatSessions/copilotcli/node/test/copilotCLISkills.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ class TestChatPromptFileService extends Disposable implements IChatPromptFileSer
3434
readonly onDidChangeCustomAgents: Event<void> = Event.None;
3535
readonly onDidChangeInstructions: Event<void> = Event.None;
3636
readonly onDidChangeSkills: Event<void> = Event.None;
37+
readonly onDidChangeHooks: Event<void> = Event.None;
3738
readonly customAgents: readonly ChatResource[] = [];
3839
readonly customAgentPromptFiles: readonly ParsedPromptFile[] = [];
3940
readonly instructions: readonly ChatResource[] = [];
4041
skills: readonly ChatResource[] = [];
42+
readonly hooks: readonly ChatResource[] = [];
4143
}
4244

4345
function createWorkspaceService(folders: URI[] = [URI.file('/workspace')]): IWorkspaceService {

src/extension/chatSessions/copilotcli/node/test/copilotCliAgents.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ class TestChatPromptFileService extends Disposable implements IChatPromptFileSer
4949
readonly onDidChangeCustomAgents: Event<void> = this._onDidChangeCustomAgents.event;
5050
readonly onDidChangeInstructions: Event<void> = Event.None;
5151
readonly onDidChangeSkills: Event<void> = Event.None;
52+
readonly onDidChangeHooks: Event<void> = Event.None;
5253
readonly customAgents: readonly import('vscode').ChatResource[] = [];
5354
readonly instructions: readonly import('vscode').ChatResource[] = [];
5455
readonly skills: readonly import('vscode').ChatResource[] = [];
56+
readonly hooks: readonly import('vscode').ChatResource[] = [];
5557

5658
constructor(private _customAgentPromptFiles: ParsedPromptFile[] = []) {
5759
super();

src/extension/chatSessions/copilotcli/vscode-node/test/testHelpers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,11 @@ export class MockChatPromptFileService extends Disposable implements IChatPrompt
237237
customAgents: ChatResource[] = [];
238238
instructions: ChatResource[] = [];
239239
skills: ChatResource[] = [];
240+
hooks: ChatResource[] = [];
240241
private readonly _onDidChangeCustomAgents = this._register(new Emitter<void>());
241242
private readonly _onDidChangeInstructions = this._register(new Emitter<void>());
242243
private readonly _onDidChangeSkills = this._register(new Emitter<void>());
244+
private readonly _onDidChangeHooks = this._register(new Emitter<void>());
243245

244246
get onDidChangeCustomAgents() {
245247
return this._onDidChangeCustomAgents.event;
@@ -252,6 +254,10 @@ export class MockChatPromptFileService extends Disposable implements IChatPrompt
252254
get onDidChangeSkills() {
253255
return this._onDidChangeSkills.event;
254256
}
257+
258+
get onDidChangeHooks() {
259+
return this._onDidChangeHooks.event;
260+
}
255261
get customAgentPromptFiles() {
256262
return [];
257263
}

src/extension/chatSessions/vscode-node/chatPromptFileService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export class ChatPromptFileService extends Disposable implements IChatPromptFile
2222
readonly onDidChangeInstructions: Event<void> = this._onDidChangeInstructions.event;
2323
private readonly _onDidChangeSkills = this._register(new Emitter<void>());
2424
readonly onDidChangeSkills: Event<void> = this._onDidChangeSkills.event;
25+
private readonly _onDidChangeHooks = this._register(new Emitter<void>());
26+
readonly onDidChangeHooks: Event<void> = this._onDidChangeHooks.event;
2527

2628
private _customAgents: ParsedPromptFile[] = [];
2729
private refreshCts: CancellationTokenSource | undefined;
@@ -43,6 +45,10 @@ export class ChatPromptFileService extends Disposable implements IChatPromptFile
4345
this._register(vscode.chat.onDidChangeSkills(() => {
4446
this._onDidChangeSkills.fire();
4547
}));
48+
49+
this._register(vscode.chat.onDidChangeHooks(() => {
50+
this._onDidChangeHooks.fire();
51+
}));
4652
this.triggerRefreshCustomAgents();
4753
}
4854

@@ -62,6 +68,10 @@ export class ChatPromptFileService extends Disposable implements IChatPromptFile
6268
return vscode.chat.skills;
6369
}
6470

71+
get hooks(): readonly vscode.ChatResource[] {
72+
return vscode.chat.hooks;
73+
}
74+
6575
override dispose(): void {
6676
this.refreshCts?.dispose(true);
6777
this.refreshCts = undefined;

src/extension/chatSessions/vscode-node/copilotCLICustomizationProvider.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod
2525
vscode.ChatSessionCustomizationType.Agent,
2626
vscode.ChatSessionCustomizationType.Skill,
2727
vscode.ChatSessionCustomizationType.Instructions,
28+
vscode.ChatSessionCustomizationType.Hook,
2829
],
2930
};
3031
}
@@ -39,19 +40,22 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod
3940
this._register(this.chatPromptFileService.onDidChangeCustomAgents(() => this._onDidChange.fire()));
4041
this._register(this.chatPromptFileService.onDidChangeInstructions(() => this._onDidChange.fire()));
4142
this._register(this.chatPromptFileService.onDidChangeSkills(() => this._onDidChange.fire()));
43+
this._register(this.chatPromptFileService.onDidChangeHooks(() => this._onDidChange.fire()));
4244
this._register(this.copilotCLIAgents.onDidChangeAgents(() => this._onDidChange.fire()));
4345
}
4446

4547
async provideChatSessionCustomizations(_token: vscode.CancellationToken): Promise<vscode.ChatSessionCustomizationItem[]> {
4648
const agents = await this.getAgentItems();
4749
const instructions = this.getInstructionItems();
4850
const skills = this.getSkillItems();
51+
const hooks = this.getHookItems();
4952

5053
this.logService.debug(`[CopilotCLICustomizationProvider] agents (${agents.length}): ${agents.map(a => a.name).join(', ') || '(none)'}`);
5154
this.logService.debug(`[CopilotCLICustomizationProvider] instructions (${instructions.length}): ${instructions.map(i => i.name).join(', ') || '(none)'}`);
5255
this.logService.debug(`[CopilotCLICustomizationProvider] skills (${skills.length}): ${skills.map(s => s.name).join(', ') || '(none)'}`);
56+
this.logService.debug(`[CopilotCLICustomizationProvider] hooks (${hooks.length}): ${hooks.map(h => h.name).join(', ') || '(none)'}`);
5357

54-
const items = [...agents, ...instructions, ...skills];
58+
const items = [...agents, ...instructions, ...skills, ...hooks];
5559
this.logService.debug(`[CopilotCLICustomizationProvider] total: ${items.length} items`);
5660
return items;
5761
}
@@ -91,6 +95,18 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod
9195
name: deriveNameFromUri(s.uri, SKILL_FILENAME),
9296
}));
9397
}
98+
99+
/**
100+
* Collects all hook items from the prompt file service.
101+
* Each item is a hook configuration file (JSON).
102+
*/
103+
private getHookItems(): vscode.ChatSessionCustomizationItem[] {
104+
return this.chatPromptFileService.hooks.map(h => ({
105+
uri: h.uri,
106+
type: vscode.ChatSessionCustomizationType.Hook,
107+
name: basename(h.uri).replace(/\.json$/i, ''),
108+
}));
109+
}
94110
}
95111

96112
function deriveNameFromUri(uri: vscode.Uri, extensionOrFilename: string): string {

src/extension/chatSessions/vscode-node/test/copilotCLICustomizationProvider.spec.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,34 @@ class MockChatPromptFileService extends mock<IChatPromptFileService>() {
5858
override readonly onDidChangeInstructions = this._onDidChangeInstructions.event;
5959
private readonly _onDidChangeSkills = new Emitter<void>();
6060
override readonly onDidChangeSkills = this._onDidChangeSkills.event;
61+
private readonly _onDidChangeHooks = new Emitter<void>();
62+
override readonly onDidChangeHooks = this._onDidChangeHooks.event;
6163

6264
private _customAgents: vscode.ChatResource[] = [];
6365
private _instructions: vscode.ChatResource[] = [];
6466
private _skills: vscode.ChatResource[] = [];
67+
private _hooks: vscode.ChatResource[] = [];
6568

6669
override get customAgents(): readonly vscode.ChatResource[] { return this._customAgents; }
6770
override get instructions(): readonly vscode.ChatResource[] { return this._instructions; }
6871
override get skills(): readonly vscode.ChatResource[] { return this._skills; }
72+
override get hooks(): readonly vscode.ChatResource[] { return this._hooks; }
6973

7074
setCustomAgents(agents: vscode.ChatResource[]) { this._customAgents = agents; }
7175
setInstructions(instructions: vscode.ChatResource[]) { this._instructions = instructions; }
7276
setSkills(skills: vscode.ChatResource[]) { this._skills = skills; }
77+
setHooks(hooks: vscode.ChatResource[]) { this._hooks = hooks; }
7378

7479
fireCustomAgentsChanged() { this._onDidChangeCustomAgents.fire(); }
7580
fireInstructionsChanged() { this._onDidChangeInstructions.fire(); }
7681
fireSkillsChanged() { this._onDidChangeSkills.fire(); }
82+
fireHooksChanged() { this._onDidChangeHooks.fire(); }
7783

7884
override dispose() {
7985
this._onDidChangeCustomAgents.dispose();
8086
this._onDidChangeInstructions.dispose();
8187
this._onDidChangeSkills.dispose();
88+
this._onDidChangeHooks.dispose();
8289
}
8390
}
8491

@@ -130,13 +137,14 @@ describe('CopilotCLICustomizationProvider', () => {
130137
expect(CopilotCLICustomizationProvider.metadata.iconId).toBe('worktree');
131138
});
132139

133-
it('supports Agent, Skill, and Instructions types', () => {
140+
it('supports Agent, Skill, Instructions, and Hook types', () => {
134141
const supported = CopilotCLICustomizationProvider.metadata.supportedTypes;
135142
expect(supported).toBeDefined();
136-
expect(supported).toHaveLength(3);
143+
expect(supported).toHaveLength(4);
137144
expect(supported).toContain(FakeChatSessionCustomizationType.Agent);
138145
expect(supported).toContain(FakeChatSessionCustomizationType.Skill);
139146
expect(supported).toContain(FakeChatSessionCustomizationType.Instructions);
147+
expect(supported).toContain(FakeChatSessionCustomizationType.Hook);
140148
});
141149

142150
it('only returns items whose type is in supportedTypes', async () => {
@@ -240,9 +248,39 @@ describe('CopilotCLICustomizationProvider', () => {
240248
mockCopilotCLIAgents.setAgents([makeAgentInfo('explore', 'Explore')]);
241249
mockPromptFileService.setInstructions([{ uri: URI.file('/workspace/.github/b.instructions.md') }]);
242250
mockPromptFileService.setSkills([{ uri: URI.file('/workspace/.github/skills/c/SKILL.md') }]);
251+
mockPromptFileService.setHooks([{ uri: URI.file('/workspace/.copilot/hooks/pre-commit.json') }]);
243252

244253
const items = await provider.provideChatSessionCustomizations(undefined!);
245-
expect(items).toHaveLength(3);
254+
expect(items).toHaveLength(4);
255+
});
256+
257+
it('returns hooks with correct type and name', async () => {
258+
const uri = URI.file('/workspace/.copilot/hooks/diagnostics.json');
259+
mockPromptFileService.setHooks([{ uri }]);
260+
261+
const items = await provider.provideChatSessionCustomizations(undefined!);
262+
expect(items).toHaveLength(1);
263+
expect(items[0].uri).toBe(uri);
264+
expect(items[0].type).toBe(FakeChatSessionCustomizationType.Hook);
265+
expect(items[0].name).toBe('diagnostics');
266+
});
267+
268+
it('strips .json extension from hook file name', async () => {
269+
mockPromptFileService.setHooks([{ uri: URI.file('/workspace/.copilot/hooks/security-checks.json') }]);
270+
271+
const items = await provider.provideChatSessionCustomizations(undefined!);
272+
expect(items[0].name).toBe('security-checks');
273+
});
274+
275+
it('returns multiple hooks', async () => {
276+
mockPromptFileService.setHooks([
277+
{ uri: URI.file('/workspace/.copilot/hooks/hooks.json') },
278+
{ uri: URI.file('/workspace/.copilot/hooks/diagnostics.json') },
279+
]);
280+
281+
const items = await provider.provideChatSessionCustomizations(undefined!);
282+
const hookItems = items.filter((i: vscode.ChatSessionCustomizationItem) => i.type === FakeChatSessionCustomizationType.Hook);
283+
expect(hookItems).toHaveLength(2);
246284
});
247285
});
248286

@@ -271,6 +309,14 @@ describe('CopilotCLICustomizationProvider', () => {
271309
expect(fired).toBe(true);
272310
});
273311

312+
it('fires when hooks change', () => {
313+
let fired = false;
314+
disposables.add(provider.onDidChange(() => { fired = true; }));
315+
316+
mockPromptFileService.fireHooksChanged();
317+
expect(fired).toBe(true);
318+
});
319+
274320
it('fires when ICopilotCLIAgents agents change', () => {
275321
let fired = false;
276322
disposables.add(provider.onDidChange(() => { fired = true; }));

src/extension/vscode.proposed.chatPromptFiles.d.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,28 @@ declare module 'vscode' {
7878

7979
// #endregion
8080

81+
// #region HookProvider
82+
83+
/**
84+
* A provider that supplies hook configuration resources (from hooks JSON files).
85+
*/
86+
export interface ChatHookProvider {
87+
/**
88+
* An optional event to signal that hooks have changed.
89+
*/
90+
readonly onDidChangeHooks?: Event<void>;
91+
92+
/**
93+
* Provide the list of hook configuration files available.
94+
* @param context Context for the provide call.
95+
* @param token A cancellation token.
96+
* @returns An array of hook resources or a promise that resolves to such.
97+
*/
98+
provideHooks(context: unknown, token: CancellationToken): ProviderResult<ChatResource[]>;
99+
}
100+
101+
// #endregion
102+
81103
// #region SkillProvider
82104

83105
/**
@@ -136,6 +158,18 @@ declare module 'vscode' {
136158
*/
137159
export const skills: readonly ChatResource[];
138160

161+
/**
162+
* An event that fires when the list of {@link hooks hooks} changes.
163+
*/
164+
export const onDidChangeHooks: Event<void>;
165+
166+
/**
167+
* The list of currently available hook configuration files.
168+
* These are JSON files that define lifecycle hooks from all sources
169+
* (workspace, user, and extension-provided).
170+
*/
171+
export const hooks: readonly ChatResource[];
172+
139173
/**
140174
* Register a provider for custom agents.
141175
* @param provider The custom agent provider.
@@ -163,6 +197,13 @@ declare module 'vscode' {
163197
* @returns A disposable that unregisters the provider when disposed.
164198
*/
165199
export function registerSkillProvider(provider: ChatSkillProvider): Disposable;
200+
201+
/**
202+
* Register a provider for hooks.
203+
* @param provider The hook provider.
204+
* @returns A disposable that unregisters the provider when disposed.
205+
*/
206+
export function registerHookProvider(provider: ChatHookProvider): Disposable;
166207
}
167208

168209
// #endregion

0 commit comments

Comments
 (0)