diff --git a/apps/emdash-desktop/src/shared/core/agents/agent-provider-registry.ts b/apps/emdash-desktop/src/shared/core/agents/agent-provider-registry.ts index 0ccd250192..da3bdcdd54 100644 --- a/apps/emdash-desktop/src/shared/core/agents/agent-provider-registry.ts +++ b/apps/emdash-desktop/src/shared/core/agents/agent-provider-registry.ts @@ -93,7 +93,6 @@ export type AgentProviderDefinition = { /** When true, the logo should be colour-inverted in dark mode. */ invertInDark?: boolean; terminalOnly?: boolean; - supportsHooks?: boolean; }; export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ @@ -122,7 +121,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'openai.svg', alt: 'Codex', terminalOnly: true, - supportsHooks: true, }, { id: 'claude', @@ -142,7 +140,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'claude.svg', alt: 'Claude Code', terminalOnly: true, - supportsHooks: true, }, { id: 'grok', @@ -164,7 +161,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ alt: 'Grok CLI', invertInDark: true, terminalOnly: true, - supportsHooks: true, }, { id: 'devin', @@ -183,7 +179,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'devin.png', alt: 'Devin', terminalOnly: true, - supportsHooks: true, }, { id: 'cursor', @@ -219,7 +214,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'gemini.svg', alt: 'Gemini CLI', terminalOnly: true, - supportsHooks: true, }, { id: 'antigravity', @@ -255,7 +249,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'qwen.svg', alt: 'Qwen Code CLI', terminalOnly: true, - supportsHooks: true, }, { id: 'droid', @@ -274,7 +267,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'droid.svg', alt: 'Factory Droid', terminalOnly: true, - supportsHooks: true, }, { id: 'amp', @@ -295,7 +287,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'ampcode.svg', alt: 'Amp CLI', terminalOnly: true, - supportsHooks: true, }, { id: 'commandcode', @@ -317,7 +308,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'commandcode.svg', alt: 'Command Code CLI', terminalOnly: true, - supportsHooks: true, }, { id: 'opencode', @@ -339,7 +329,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ iconDark: 'opencode-dark.svg', alt: 'OpenCode CLI', terminalOnly: true, - supportsHooks: true, }, { id: 'hermes', @@ -379,7 +368,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ alt: 'GitHub Copilot CLI', invertInDark: true, terminalOnly: true, - supportsHooks: true, }, { id: 'charm', @@ -433,7 +421,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'goose.png', alt: 'Goose CLI', terminalOnly: true, - supportsHooks: true, }, { id: 'kimi', @@ -454,7 +441,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'kimi.svg', alt: 'Kimi CLI', terminalOnly: true, - supportsHooks: true, }, { id: 'kilocode', @@ -472,7 +458,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'kilocode.png', alt: 'Kilocode CLI', terminalOnly: true, - supportsHooks: true, }, { id: 'kiro', @@ -494,7 +479,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'kiro.png', alt: 'Kiro CLI', terminalOnly: true, - supportsHooks: true, }, { id: 'rovo', @@ -589,7 +573,6 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [ icon: 'mistral.svg', alt: 'Mistral Vibe CLI', terminalOnly: true, - supportsHooks: true, }, { id: 'jules', diff --git a/packages/core/src/agents/plugins/capabilities/hooks-types.ts b/packages/core/src/agents/plugins/capabilities/hooks-types.ts index f8c44ecfc9..6ea1b9fd6f 100644 --- a/packages/core/src/agents/plugins/capabilities/hooks-types.ts +++ b/packages/core/src/agents/plugins/capabilities/hooks-types.ts @@ -1,6 +1,7 @@ export const HOOK_EVENTS = [ 'notification', 'stop', + 'error', 'session', 'start', 'tool-use', diff --git a/packages/core/src/agents/plugins/capabilities/hooks.ts b/packages/core/src/agents/plugins/capabilities/hooks.ts index fc71065da8..cd5d647c6b 100644 --- a/packages/core/src/agents/plugins/capabilities/hooks.ts +++ b/packages/core/src/agents/plugins/capabilities/hooks.ts @@ -1,7 +1,7 @@ import z from 'zod'; import { definePluginCapability } from '../../../lib/plugins/capability'; import type { PluginFs } from '../../runtime/fs'; -import type { CanonicalHookEvent, HookRegistration } from './hooks-types'; +import { HOOK_EVENTS, type CanonicalHookEvent, type HookRegistration } from './hooks-types'; export type { HookRegistration }; export type { CanonicalHookEvent, HookEvent, NotificationType } from './hooks-types'; @@ -20,6 +20,8 @@ export type IHooksBehavior = { parseHookEvent?(eventType: string, body: Record): CanonicalHookEvent; }; +const hookEventSchema = z.enum(HOOK_EVENTS); + /** * hooksDescriptor is used to describe the hooks that an agent supports. * @@ -33,16 +35,12 @@ export const hooksCapability = definePluginCapability()( z.object({ kind: z.literal('config'), scope: z.enum(['global', 'workspace']), - supportedEvents: z.array( - z.enum(['notification', 'stop', 'session', 'start', 'tool-use', 'tool-use-failure']) - ), + supportedEvents: z.array(hookEventSchema), }), z.object({ kind: z.literal('plugin'), scope: z.enum(['global', 'workspace']), - supportedEvents: z.array( - z.enum(['notification', 'stop', 'session', 'start', 'tool-use', 'tool-use-failure']) - ), + supportedEvents: z.array(hookEventSchema), }), z.object({ kind: z.literal('none') }), ]), diff --git a/packages/core/src/agents/plugins/helpers/parse-hook-event.test.ts b/packages/core/src/agents/plugins/helpers/parse-hook-event.test.ts new file mode 100644 index 0000000000..7bfc16c735 --- /dev/null +++ b/packages/core/src/agents/plugins/helpers/parse-hook-event.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { defaultHookEventParser } from './parse-hook-event'; + +describe('defaultHookEventParser', () => { + it('maps tool-use events to working status events', () => { + expect(defaultHookEventParser('tool-use', { message: 'ran shell' })).toEqual({ + kind: 'status', + type: 'start', + lastAssistantMessage: undefined, + title: undefined, + message: 'ran shell', + }); + }); + + it('maps tool-use-failure events to error status events', () => { + expect(defaultHookEventParser('tool-use-failure', { title: 'Tool failed' })).toEqual({ + kind: 'status', + type: 'error', + lastAssistantMessage: undefined, + title: 'Tool failed', + message: undefined, + }); + }); +}); diff --git a/packages/core/src/agents/plugins/helpers/parse-hook-event.ts b/packages/core/src/agents/plugins/helpers/parse-hook-event.ts index 4c294140d5..a09f0a1ed7 100644 --- a/packages/core/src/agents/plugins/helpers/parse-hook-event.ts +++ b/packages/core/src/agents/plugins/helpers/parse-hook-event.ts @@ -36,6 +36,8 @@ export function extractProviderSessionId(body: Record): string * Event-type routing: * 'session' | 'session-start' → kind: 'session' (if a session id is present) * 'start' | 'stop' | 'error' → kind: 'status' with the matching type + * 'tool-use' → kind: 'status', type: 'start' + * 'tool-use-failure' → kind: 'status', type: 'error' * 'notification' → kind: 'status', type: 'notification', reads * notification_type / notificationType from body * everything else → kind: 'ignore' @@ -50,11 +52,14 @@ export function defaultHookEventParser( return { kind: 'ignore' }; } - if (eventType === 'start' || eventType === 'stop' || eventType === 'error') { + const statusType = + eventType === 'tool-use' ? 'start' : eventType === 'tool-use-failure' ? 'error' : eventType; + + if (statusType === 'start' || statusType === 'stop' || statusType === 'error') { const rawLam = body.last_assistant_message ?? body.lastAssistantMessage; return { kind: 'status', - type: eventType, + type: statusType, lastAssistantMessage: typeof rawLam === 'string' ? rawLam : undefined, title: typeof body.title === 'string' ? body.title : undefined, message: typeof body.message === 'string' ? body.message : undefined, diff --git a/packages/plugins/src/agents/impl/claude/hooks.test.ts b/packages/plugins/src/agents/impl/claude/hooks.test.ts new file mode 100644 index 0000000000..32bcf2b068 --- /dev/null +++ b/packages/plugins/src/agents/impl/claude/hooks.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { buildClaudeHookConfig } from './hooks'; + +describe('buildClaudeHookConfig', () => { + it('uses explicit notification_type before message heuristics', () => { + const hooks = buildClaudeHookConfig(); + + expect( + hooks.parseHookEvent('notification', { notification_type: 'permission_prompt' }) + ).toEqual({ + kind: 'status', + type: 'notification', + notificationType: 'permission_prompt', + lastAssistantMessage: undefined, + title: undefined, + message: undefined, + }); + }); + + it('classifies Claude notification messages when no notification_type is present', () => { + const hooks = buildClaudeHookConfig(); + + expect(hooks.parseHookEvent('notification', { message: 'Claude needs approval' })).toEqual({ + kind: 'status', + type: 'notification', + notificationType: 'permission_prompt', + message: 'Claude needs approval', + title: undefined, + }); + }); +}); diff --git a/packages/plugins/src/agents/impl/claude/hooks.ts b/packages/plugins/src/agents/impl/claude/hooks.ts index 97772ab97c..dfb783969b 100644 --- a/packages/plugins/src/agents/impl/claude/hooks.ts +++ b/packages/plugins/src/agents/impl/claude/hooks.ts @@ -2,14 +2,15 @@ import type { CanonicalHookEvent } from '@emdash/core/agents/plugins'; import { buildNestedJsonHookConfig, defaultHookEventParser, + makeNotificationHookCommand, makeStdinHookCommand, } from '@emdash/core/agents/plugins/helpers'; export const CLAUDE_SETTINGS_PATH = '.claude/settings.local.json'; /** - * Claude's Notification events carry no `notification_type` field. - * Classify by examining the message text: + * Claude's Notification events usually carry no `notification_type` field. + * Classify those by examining the message text: * /permission|approval/i → permission_prompt * everything else → idle_prompt (agent waiting / done) */ @@ -18,6 +19,10 @@ function parseClaudeHookEvent( body: Record ): CanonicalHookEvent { if (eventType === 'notification') { + if (body.notification_type || body.notificationType) { + return defaultHookEventParser(eventType, body); + } + const message = typeof body.message === 'string' ? body.message : ''; const notificationType = /permission|approval/i.test(message) ? ('permission_prompt' as const) @@ -37,9 +42,13 @@ function parseClaudeHookEvent( export function buildClaudeHookConfig() { return { ...buildNestedJsonHookConfig(CLAUDE_SETTINGS_PATH, [ + { hookKey: 'SessionStart', command: makeStdinHookCommand('session') }, { hookKey: 'UserPromptSubmit', command: makeStdinHookCommand('start') }, + { hookKey: 'PermissionRequest', command: makeNotificationHookCommand('permission_prompt') }, { hookKey: 'Notification', command: makeStdinHookCommand('notification') }, { hookKey: 'Stop', command: makeStdinHookCommand('stop') }, + { hookKey: 'StopFailure', command: makeStdinHookCommand('error') }, + { hookKey: 'SessionEnd', command: makeStdinHookCommand('stop') }, ]), parseHookEvent: parseClaudeHookEvent, }; diff --git a/packages/plugins/src/agents/impl/claude/index.ts b/packages/plugins/src/agents/impl/claude/index.ts index 8ec9cef873..3c33f17be1 100644 --- a/packages/plugins/src/agents/impl/claude/index.ts +++ b/packages/plugins/src/agents/impl/claude/index.ts @@ -22,7 +22,7 @@ export const plugin = definePlugin( hooks: { kind: 'config', scope: 'workspace', - supportedEvents: ['start', 'notification', 'stop'], + supportedEvents: ['start', 'notification', 'stop', 'error', 'session'], }, hostDependency: { id: 'claude', diff --git a/packages/plugins/src/agents/impl/copilot/hooks.test.ts b/packages/plugins/src/agents/impl/copilot/hooks.test.ts new file mode 100644 index 0000000000..229bb75c9b --- /dev/null +++ b/packages/plugins/src/agents/impl/copilot/hooks.test.ts @@ -0,0 +1,78 @@ +import type { PluginFs } from '@emdash/core/agents/plugins'; +import { EMDASH_MARKER } from '@emdash/core/agents/plugins/helpers'; +import { describe, expect, it } from 'vitest'; +import { COPILOT_HOOKS_PATH, buildCopilotHookConfig } from './hooks'; + +function createMemoryFs(initial: Record = {}): PluginFs { + const files = new Map(Object.entries(initial)); + + return { + async read(path) { + return files.get(path) ?? null; + }, + async write(path, content) { + files.set(path, content); + }, + async delete(path) { + files.delete(path); + }, + async exists(path) { + return files.has(path); + }, + async list(path) { + return [...files.keys()].filter((file) => file.startsWith(path)); + }, + }; +} + +function copilotConfigWithHooks(hookKeys: string[]): string { + return JSON.stringify({ + hooks: Object.fromEntries( + hookKeys.map((hookKey) => [ + hookKey, + [ + { + type: 'command', + command: 'curl http://127.0.0.1:$EMDASH_HOOK_PORT/hook', + }, + ], + ]) + ), + }); +} + +describe('buildCopilotHookConfig', () => { + it('does not treat partial managed hook installs as installed', async () => { + const fs = createMemoryFs({ + [COPILOT_HOOKS_PATH]: copilotConfigWithHooks([ + 'agentStop', + 'sessionStart', + 'permissionRequest', + ]), + }); + const hooks = buildCopilotHookConfig(); + + await expect(hooks.getHooksInstalled(fs)).resolves.toBe(false); + await expect(hooks.readHooks(fs)).resolves.toEqual([]); + }); + + it('treats a complete managed hook install as installed', async () => { + const fs = createMemoryFs({ + [COPILOT_HOOKS_PATH]: copilotConfigWithHooks([ + 'agentStop', + 'sessionEnd', + 'sessionStart', + 'userPromptSubmitted', + 'errorOccurred', + 'notification', + 'permissionRequest', + ]), + }); + const hooks = buildCopilotHookConfig(); + + await expect(hooks.getHooksInstalled(fs)).resolves.toBe(true); + await expect(hooks.readHooks(fs)).resolves.toEqual([ + { event: 'emdash', command: EMDASH_MARKER }, + ]); + }); +}); diff --git a/packages/plugins/src/agents/impl/copilot/hooks.ts b/packages/plugins/src/agents/impl/copilot/hooks.ts index 36d2cb1cf8..b7c5455a7e 100644 --- a/packages/plugins/src/agents/impl/copilot/hooks.ts +++ b/packages/plugins/src/agents/impl/copilot/hooks.ts @@ -12,20 +12,36 @@ import { export const COPILOT_HOOKS_PATH = '.github/hooks/emdash.json'; +const COPILOT_MANAGED_HOOK_KEYS = [ + 'agentStop', + 'sessionEnd', + 'sessionStart', + 'userPromptSubmitted', + 'errorOccurred', + 'notification', + 'permissionRequest', +]; + +function hasAllManagedCopilotHooks(hooks: Record): boolean { + return COPILOT_MANAGED_HOOK_KEYS.every((k) => { + const entries = Array.isArray(hooks[k]) ? hooks[k] : []; + return entries.some((e) => JSON.stringify(e).includes(EMDASH_MARKER)); + }); +} + export function buildCopilotHookConfig() { const stopCmd = makeStdinHookCommand('stop'); + const startCmd = makeStdinHookCommand('start'); const sessionCmd = makeStdinHookCommand('session'); + const errorCmd = makeStdinHookCommand('error'); + const notificationCmd = makeStdinHookCommand('notification'); const permCmd = makeNotificationHookCommand('permission_prompt'); return { async readHooks(fs: PluginFs): Promise { const config = await readJsonConfig(fs, COPILOT_HOOKS_PATH); const hooks = (config.hooks ?? {}) as Record; - const installed = ['agentStop', 'sessionStart', 'permissionRequest'].some((k) => { - const entries = Array.isArray(hooks[k]) ? hooks[k] : []; - return entries.some((e) => JSON.stringify(e).includes(EMDASH_MARKER)); - }); - return installed ? [{ event: 'emdash', command: EMDASH_MARKER }] : []; + return hasAllManagedCopilotHooks(hooks) ? [{ event: 'emdash', command: EMDASH_MARKER }] : []; }, async writeHooks(fs: PluginFs, _hooks: HookRegistration[]): Promise { const config = await readJsonConfig(fs, COPILOT_HOOKS_PATH); @@ -36,19 +52,38 @@ export function buildCopilotHookConfig() { ...filterUserHooks(stopExisting as Record[]), buildFlatEntry(stopCmd), ]; + const sessionEndExisting = Array.isArray(hooks.sessionEnd) ? hooks.sessionEnd : []; + hooks.sessionEnd = [ + ...filterUserHooks(sessionEndExisting as Record[]), + buildFlatEntry(stopCmd), + ]; const sessionExisting = Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []; hooks.sessionStart = [ ...filterUserHooks(sessionExisting as Record[]), buildFlatEntry(sessionCmd), ]; + const startExisting = Array.isArray(hooks.userPromptSubmitted) + ? hooks.userPromptSubmitted + : []; + hooks.userPromptSubmitted = [ + ...filterUserHooks(startExisting as Record[]), + buildFlatEntry(startCmd), + ]; + const errorExisting = Array.isArray(hooks.errorOccurred) ? hooks.errorOccurred : []; + hooks.errorOccurred = [ + ...filterUserHooks(errorExisting as Record[]), + buildFlatEntry(errorCmd), + ]; + const notificationExisting = Array.isArray(hooks.notification) ? hooks.notification : []; + hooks.notification = [ + ...filterUserHooks(notificationExisting as Record[]), + buildFlatEntry(notificationCmd), + ]; const permExisting = Array.isArray(hooks.permissionRequest) ? hooks.permissionRequest : []; hooks.permissionRequest = [ ...filterUserHooks(permExisting as Record[]), buildFlatEntry(permCmd), ]; - if (Array.isArray(hooks.notification)) { - hooks.notification = filterUserHooks(hooks.notification as Record[]); - } await writeJsonConfig(fs, COPILOT_HOOKS_PATH, { ...config, version: 1, hooks }); return [COPILOT_HOOKS_PATH]; @@ -64,10 +99,7 @@ export function buildCopilotHookConfig() { async getHooksInstalled(fs: PluginFs): Promise { const config = await readJsonConfig(fs, COPILOT_HOOKS_PATH); const hooks = (config.hooks ?? {}) as Record; - return ['agentStop', 'sessionStart', 'permissionRequest'].some((k) => { - const entries = Array.isArray(hooks[k]) ? hooks[k] : []; - return entries.some((e) => JSON.stringify(e).includes(EMDASH_MARKER)); - }); + return hasAllManagedCopilotHooks(hooks); }, }; } diff --git a/packages/plugins/src/agents/impl/copilot/index.ts b/packages/plugins/src/agents/impl/copilot/index.ts index bc7e5341d0..6e2e721211 100644 --- a/packages/plugins/src/agents/impl/copilot/index.ts +++ b/packages/plugins/src/agents/impl/copilot/index.ts @@ -22,7 +22,7 @@ export const plugin = definePlugin( hooks: { kind: 'config', scope: 'workspace', - supportedEvents: ['stop', 'session', 'notification'], + supportedEvents: ['start', 'stop', 'error', 'session', 'notification'], }, hostDependency: npmDependency({ id: 'copilot', package: '@github/copilot' }), mcp: { diff --git a/packages/plugins/src/agents/impl/devin/hooks.ts b/packages/plugins/src/agents/impl/devin/hooks.ts index 63be0f5667..a53b28698c 100644 --- a/packages/plugins/src/agents/impl/devin/hooks.ts +++ b/packages/plugins/src/agents/impl/devin/hooks.ts @@ -8,6 +8,8 @@ export const DEVIN_HOOKS_PATH = '.devin/hooks.v1.json'; export function buildDevinHookConfig() { return buildNestedJsonHookConfig(DEVIN_HOOKS_PATH, [ + { hookKey: 'SessionStart', command: makeStdinHookCommand('session') }, + { hookKey: 'UserPromptSubmit', command: makeStdinHookCommand('start') }, { hookKey: 'Stop', command: makeStdinHookCommand('stop') }, { hookKey: 'SessionEnd', command: makeStdinHookCommand('stop') }, { hookKey: 'PermissionRequest', command: makeNotificationHookCommand('permission_prompt') }, diff --git a/packages/plugins/src/agents/impl/devin/index.ts b/packages/plugins/src/agents/impl/devin/index.ts index 0bbe391d61..b046c17e8e 100644 --- a/packages/plugins/src/agents/impl/devin/index.ts +++ b/packages/plugins/src/agents/impl/devin/index.ts @@ -18,7 +18,7 @@ export const plugin = definePlugin( hooks: { kind: 'config', scope: 'workspace', - supportedEvents: ['stop', 'notification'], + supportedEvents: ['start', 'stop', 'notification', 'session'], }, hostDependency: { id: 'devin', diff --git a/packages/plugins/src/agents/impl/droid/hooks.ts b/packages/plugins/src/agents/impl/droid/hooks.ts index aa2f25b9ab..181542eeab 100644 --- a/packages/plugins/src/agents/impl/droid/hooks.ts +++ b/packages/plugins/src/agents/impl/droid/hooks.ts @@ -14,7 +14,9 @@ const DROID_HOOK_SPECS = [ { hookKey: 'UserPromptSubmit', command: makeStdinHookCommand('start') }, { hookKey: 'Notification', command: makeStdinHookCommand('notification') }, { hookKey: 'Stop', command: makeStdinHookCommand('stop') }, + { hookKey: 'SubagentStop', command: makeStdinHookCommand('stop') }, { hookKey: 'SessionStart', command: makeStdinHookCommand('session') }, + { hookKey: 'SessionEnd', command: makeStdinHookCommand('stop') }, ]; function getHooks(config: Record): Record { diff --git a/packages/plugins/src/agents/impl/kilocode/index.ts b/packages/plugins/src/agents/impl/kilocode/index.ts index 2ff64f21f0..c7f0934bb4 100644 --- a/packages/plugins/src/agents/impl/kilocode/index.ts +++ b/packages/plugins/src/agents/impl/kilocode/index.ts @@ -24,7 +24,7 @@ export const plugin = definePlugin( hooks: { kind: 'plugin', scope: 'workspace', - supportedEvents: ['notification', 'stop', 'session'], + supportedEvents: ['notification', 'error', 'session'], }, hostDependency: npmDependency({ id: 'kilocode', diff --git a/packages/plugins/src/agents/impl/mistral/hooks.test.ts b/packages/plugins/src/agents/impl/mistral/hooks.test.ts index cb71008edb..5c28505763 100644 --- a/packages/plugins/src/agents/impl/mistral/hooks.test.ts +++ b/packages/plugins/src/agents/impl/mistral/hooks.test.ts @@ -50,4 +50,15 @@ command = "curl http://127.0.0.1:$EMDASH_HOOK_PORT/hook" ); await expect(fs.exists(MISTRAL_HOOKS_PATH)).resolves.toBe(false); }); + + it('excludes ask_user_question from the after_tool hook', async () => { + const fs = createMemoryFs({ [MISTRAL_CONFIG_PATH]: '' }); + const hooks = buildMistralHookConfig(); + + await hooks.writeHooks(fs, []); + + const hooksToml = await fs.read(MISTRAL_HOOKS_PATH); + expect(hooksToml).toContain('name = "emdash-after-tool"'); + expect(hooksToml).toContain('match = "re:^(?!ask_user_question$).+"'); + }); }); diff --git a/packages/plugins/src/agents/impl/mistral/hooks.ts b/packages/plugins/src/agents/impl/mistral/hooks.ts index a142c7cbe1..37e5723fb2 100644 --- a/packages/plugins/src/agents/impl/mistral/hooks.ts +++ b/packages/plugins/src/agents/impl/mistral/hooks.ts @@ -28,6 +28,15 @@ const MISTRAL_HOOK_ENTRIES = [ strict: false, description: 'Notify Emdash when Mistral Vibe asks for user input.', }, + { + name: 'emdash-after-tool', + type: 'after_tool', + match: 're:^(?!ask_user_question$).+', + command: makeStdinHookCommand('tool-use'), + timeout: 10, + strict: false, + description: 'Notify Emdash after Mistral Vibe runs a non-prompt tool.', + }, ]; async function enableExperimentalHooks(fs: PluginFs): Promise { diff --git a/packages/plugins/src/agents/impl/mistral/index.ts b/packages/plugins/src/agents/impl/mistral/index.ts index 6ebb555a54..12073f2d0a 100644 --- a/packages/plugins/src/agents/impl/mistral/index.ts +++ b/packages/plugins/src/agents/impl/mistral/index.ts @@ -18,7 +18,7 @@ export const plugin = definePlugin( hooks: { kind: 'config', scope: 'workspace', - supportedEvents: ['notification', 'stop'], + supportedEvents: ['notification', 'stop', 'tool-use'], }, hostDependency: { id: 'mistral', diff --git a/packages/plugins/src/agents/impl/pi/index.ts b/packages/plugins/src/agents/impl/pi/index.ts index 479672caca..24f271ab34 100644 --- a/packages/plugins/src/agents/impl/pi/index.ts +++ b/packages/plugins/src/agents/impl/pi/index.ts @@ -21,7 +21,7 @@ export const plugin = definePlugin( hooks: { kind: 'plugin', scope: 'workspace', - supportedEvents: ['stop'], + supportedEvents: ['stop', 'error'], }, hostDependency: npmDependency({ id: 'pi',