Compare commits

...

6 Commits

Author SHA1 Message Date
Jarrod Whelan 957f5639ba limit turning off message suppression to the current turn only 2026-04-07 13:51:20 -07:00
Jarrod Whelan 75adad8018 fix(cli): prevent suppression of history items during tool confirmation
Updates the history item suppression logic in MainContent to ensure that
intermediate turns and narration text remain visible while a tool
confirmation (like ask_user) is active. This prevents items from
unexpectedly disappearing from the UI during interactive tool execution.

- Modified MainContent.tsx to skip suppression when showConfirmationQueue is true.
- Updated MainContent.test.tsx with a default mock for useConfirmingTool.
2026-04-07 13:45:38 -07:00
Enjoy Kumawat ab3075feb9 fix: use directory junctions on Windows for skill linking (#24823) 2026-04-07 19:28:43 +00:00
Abhi 5588000e93 chore: fix formatting for behavioral eval skill reference file (#24846) 2026-04-07 19:26:53 +00:00
krishdef7 68fef8745e fix(core): propagate BeforeModel hook model override end-to-end (#24784)
Signed-off-by: krishdef7 <gargkrish06@gmail.com>
Co-authored-by: Sandy Tao <sandytao520@icloud.com>
2026-04-07 17:49:26 +00:00
Michael Bleigh e432f7c009 feat(hooks): display hook system messages in UI (#24616) 2026-04-07 17:42:39 +00:00
17 changed files with 283 additions and 101 deletions
@@ -33,16 +33,35 @@ evaluation.
- **Warning**: Do not lose test fidelity by making prompts too direct/easy.
- **Primary Fix Trigger**: Adjust tool descriptions, system prompts
(`snippets.ts`), or **modules that contribute to the prompt template**.
- Fixes should generally try to improve the prompt `@packages/core/src/prompts/snippets.ts` first.
- **Instructional Generality**: Changes to the system prompt should aim to be as general as possible while still accomplishing the goal. Specificity should be added only as needed.
- **Principle**: Instead of creating "forbidden lists" for specific syntax (e.g., "Don't use `Object.create()`"), formulate a broader engineering principle that covers the underlying issue (e.g., "Prioritize explicit composition over hidden prototype manipulation"). This improves steerability across a wider range of similar scenarios.
- *Low Specificity*: "Follow ecosystem best practices"
- *Medium Specificity*: "Utilize OOP and functional best practices, as applicable"
- *High Specificity*: Provide ecosystem-specific hints as examples of a broader principle rather than direct instructions. e.g., "NEVER use hacks like bypassing the type system or employing 'hidden' logic (e.g.: reflection, prototype manipulation). Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity."
- **Prompt Simplification**: Once the test is passing, use `ask_user` to determine if prompt simplification is desired.
- **Criteria**: Simplification should be attempted only if there are related clauses that can be de-duplicated or reparented under a single heading.
- **Verification**: As part of simplification, you MUST identify and run any behavioral eval tests that might be affected by the changes to ensure no regressions are introduced.
- Test fixes should not "cheat" by changing a test's `GEMINI.md` file or by updating the test's prompt to instruct it to not repro the bug.
- Fixes should generally try to improve the prompt
`@packages/core/src/prompts/snippets.ts` first.
- **Instructional Generality**: Changes to the system prompt should aim to
be as general as possible while still accomplishing the goal. Specificity
should be added only as needed.
- **Principle**: Instead of creating "forbidden lists" for specific syntax
(e.g., "Don't use `Object.create()`"), formulate a broader engineering
principle that covers the underlying issue (e.g., "Prioritize explicit
composition over hidden prototype manipulation"). This improves
steerability across a wider range of similar scenarios.
- _Low Specificity_: "Follow ecosystem best practices"
- _Medium Specificity_: "Utilize OOP and functional best practices, as
applicable"
- _High Specificity_: Provide ecosystem-specific hints as examples of a
broader principle rather than direct instructions. e.g., "NEVER use
hacks like bypassing the type system or employing 'hidden' logic (e.g.:
reflection, prototype manipulation). Instead, use explicit and idiomatic
language features (e.g.: type guards, explicit class instantiation, or
object spread) that maintain structural integrity."
- **Prompt Simplification**: Once the test is passing, use `ask_user` to
determine if prompt simplification is desired.
- **Criteria**: Simplification should be attempted only if there are
related clauses that can be de-duplicated or reparented under a single
heading.
- **Verification**: As part of simplification, you MUST identify and run
any behavioral eval tests that might be affected by the changes to
ensure no regressions are introduced.
- Test fixes should not "cheat" by changing a test's `GEMINI.md` file or by
updating the test's prompt to instruct it to not repro the bug.
- **Warning**: Prompts have multiple configurations; ensure your fix targets
the correct config for the model in question.
4. **Architecture Options**: If prompt or instruction tuning triggers no
+17 -1
View File
@@ -36,9 +36,11 @@ import {
type ConfirmationRequest,
type PermissionConfirmationRequest,
type QuotaStats,
MessageType,
StreamingState,
type HistoryItemInfo,
} from './types.js';
import { checkPermissions } from './hooks/atCommandProcessor.js';
import { MessageType, StreamingState } from './types.js';
import { ToolActionsProvider } from './contexts/ToolActionsContext.js';
import { MouseProvider } from './contexts/MouseContext.js';
import { ScrollProvider } from './contexts/ScrollProvider.js';
@@ -51,6 +53,7 @@ import {
type UserTierId,
type GeminiUserTier,
type UserFeedbackPayload,
type HookSystemMessagePayload,
type AgentDefinition,
type ApprovalMode,
IdeClient,
@@ -2111,7 +2114,19 @@ Logging in with Google... Restarting Gemini CLI to continue.
}
};
const handleHookSystemMessage = (payload: HookSystemMessagePayload) => {
historyManager.addItem(
{
type: MessageType.INFO,
text: payload.message,
source: payload.hookName,
} as HistoryItemInfo,
Date.now(),
);
};
coreEvents.on(CoreEvent.UserFeedback, handleUserFeedback);
coreEvents.on(CoreEvent.HookSystemMessage, handleHookSystemMessage);
// Flush any messages that happened during startup before this component
// mounted.
@@ -2119,6 +2134,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
return () => {
coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback);
coreEvents.off(CoreEvent.HookSystemMessage, handleHookSystemMessage);
};
}, [historyManager]);
@@ -134,6 +134,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
<InfoMessage
text={itemForDisplay.text}
secondaryText={itemForDisplay.secondaryText}
source={itemForDisplay.source}
icon={itemForDisplay.icon}
color={itemForDisplay.color}
marginBottom={itemForDisplay.marginBottom}
@@ -66,7 +66,7 @@ vi.mock('../hooks/useAlternateBuffer.js', () => ({
}));
vi.mock('../hooks/useConfirmingTool.js', () => ({
useConfirmingTool: vi.fn(),
useConfirmingTool: vi.fn(() => null),
}));
vi.mock('./AppHeader.js', () => ({
@@ -882,6 +882,82 @@ describe('MainContent', () => {
expect(output).toContain('Here is your answer.');
unmount();
});
it('bypasses suppression for the current turn during confirmation, but keeps old turns suppressed', async () => {
const { useConfirmingTool } = await import(
'../hooks/useConfirmingTool.js'
);
vi.mocked(useConfirmingTool).mockReturnValue({
tool: {
callId: 'call-1',
name: 'some_tool',
status: CoreToolCallStatus.AwaitingApproval,
confirmationDetails: {
type: 'confirm_shell_command' as const,
command: 'echo "hello"',
},
},
index: 0,
total: 1,
} as unknown as ConfirmingToolState);
mockUseSettings.mockReturnValue(settingsWithNarration);
const uiState = {
...defaultMockUiState,
history: [
// Previous turn: should remain suppressed
{ id: 10, type: 'user' as const, text: 'Old Turn' },
{ id: 11, type: 'gemini' as const, text: 'Old narration' },
{
id: 12,
type: 'tool_group' as const,
tools: [
{
callId: 'old',
name: 'ls',
status: CoreToolCallStatus.Success,
description: '',
resultDisplay: '',
confirmationDetails: undefined,
} as IndividualToolCallDisplay,
],
},
// Current turn: should be bypassed (shown)
{ id: 20, type: 'user' as const, text: 'Current Turn' },
{ id: 21, type: 'gemini' as const, text: 'Current narration' },
],
pendingHistoryItems: [
{
type: 'tool_group' as const,
tools: [
{
callId: 'call-1',
name: 'some_tool',
status: CoreToolCallStatus.AwaitingApproval,
description: '',
resultDisplay: '',
confirmationDetails: undefined,
} as IndividualToolCallDisplay,
],
},
],
};
const { lastFrame, unmount } = await renderWithProviders(
<MainContent />,
{
uiState: uiState as Partial<UIState>,
settings: settingsWithNarration,
},
);
const output = lastFrame();
// Old narration should STILL be suppressed
expect(output).not.toContain('Old narration');
// Current narration should be VISIBLE because of the bypass
expect(output).toContain('Current narration');
unmount();
});
});
it('renders multiple thinking messages sequentially correctly', async () => {
+15 -3
View File
@@ -123,20 +123,32 @@ export const MainContent = () => {
// Rule 2: Suppress text in intermediate turns (turns containing non-topic
// tools) to hide mechanical narration.
if (turnIsIntermediate) {
const isCurrentTurn = i > lastUserPromptIndex;
// Scoping the bypass to the current turn prevents old narration from reappearing.
// This logic affects the rendered height of the history list by conditionally hiding turns.
const bypassSuppression = isCurrentTurn && showConfirmationQueue;
if (turnIsIntermediate && !bypassSuppression) {
flags[i] = true;
}
// Rule 3: Suppress text that precedes a topic tool in the same turn,
// as the topic tool "replaces" it.
if (hasTopicToolInTurn) {
if (hasTopicToolInTurn && !bypassSuppression) {
flags[i] = true;
}
}
}
}
return flags;
}, [uiState.history, pendingHistoryItems, topicUpdateNarrationEnabled]);
}, [
uiState.history,
pendingHistoryItems,
topicUpdateNarrationEnabled,
showConfirmationQueue,
lastUserPromptIndex,
]);
const augmentedHistory = useMemo(
() =>
@@ -12,6 +12,7 @@ import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
interface InfoMessageProps {
text: string;
secondaryText?: string;
source?: string;
icon?: string;
color?: string;
marginBottom?: number;
@@ -20,6 +21,7 @@ interface InfoMessageProps {
export const InfoMessage: React.FC<InfoMessageProps> = ({
text,
secondaryText,
source,
icon,
color,
marginBottom,
@@ -40,6 +42,9 @@ export const InfoMessage: React.FC<InfoMessageProps> = ({
{index === text.split('\n').length - 1 && secondaryText && (
<Text color={theme.text.secondary}> {secondaryText}</Text>
)}
{index === text.split('\n').length - 1 && source && (
<Text color={theme.text.secondary}> [{source}]</Text>
)}
</Text>
))}
</Box>
+1
View File
@@ -174,6 +174,7 @@ export type HistoryItemInfo = HistoryItemBase & {
type: 'info';
text: string;
secondaryText?: string;
source?: string;
icon?: string;
color?: string;
marginBottom?: number;
+65 -81
View File
@@ -26,66 +26,49 @@ describe('skillUtils', () => {
vi.unstubAllEnvs();
});
const itif = (condition: boolean) => (condition ? it : it.skip);
describe('linkSkill', () => {
// TODO: issue 19388 - Enable linkSkill tests on Windows
itif(process.platform !== 'win32')(
'should successfully link from a local directory',
async () => {
// Create a mock skill directory
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
const skillSubDir = path.join(mockSkillSourceDir, 'test-skill');
await fs.mkdir(skillSubDir, { recursive: true });
await fs.writeFile(
path.join(skillSubDir, 'SKILL.md'),
'---\nname: test-skill\ndescription: test\n---\nbody',
);
it('should successfully link from a local directory', async () => {
// Create a mock skill directory
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
const skillSubDir = path.join(mockSkillSourceDir, 'test-skill');
await fs.mkdir(skillSubDir, { recursive: true });
await fs.writeFile(
path.join(skillSubDir, 'SKILL.md'),
'---\nname: test-skill\ndescription: test\n---\nbody',
);
const skills = await linkSkill(
mockSkillSourceDir,
'workspace',
() => {},
);
expect(skills.length).toBe(1);
expect(skills[0].name).toBe('test-skill');
const skills = await linkSkill(mockSkillSourceDir, 'workspace', () => {});
expect(skills.length).toBe(1);
expect(skills[0].name).toBe('test-skill');
const linkedPath = path.join(tempDir, '.gemini/skills', 'test-skill');
const stats = await fs.lstat(linkedPath);
expect(stats.isSymbolicLink()).toBe(true);
const linkedPath = path.join(tempDir, '.gemini/skills', 'test-skill');
const stats = await fs.lstat(linkedPath);
expect(stats.isSymbolicLink()).toBe(true);
const linkTarget = await fs.readlink(linkedPath);
expect(path.resolve(linkTarget)).toBe(path.resolve(skillSubDir));
},
);
const linkTarget = await fs.readlink(linkedPath);
expect(path.resolve(linkTarget)).toBe(path.resolve(skillSubDir));
});
itif(process.platform !== 'win32')(
'should overwrite existing skill at destination',
async () => {
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
const skillSubDir = path.join(mockSkillSourceDir, 'test-skill');
await fs.mkdir(skillSubDir, { recursive: true });
await fs.writeFile(
path.join(skillSubDir, 'SKILL.md'),
'---\nname: test-skill\ndescription: test\n---\nbody',
);
it('should overwrite existing skill at destination', async () => {
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
const skillSubDir = path.join(mockSkillSourceDir, 'test-skill');
await fs.mkdir(skillSubDir, { recursive: true });
await fs.writeFile(
path.join(skillSubDir, 'SKILL.md'),
'---\nname: test-skill\ndescription: test\n---\nbody',
);
const targetDir = path.join(tempDir, '.gemini/skills');
await fs.mkdir(targetDir, { recursive: true });
const existingPath = path.join(targetDir, 'test-skill');
await fs.mkdir(existingPath);
const targetDir = path.join(tempDir, '.gemini/skills');
await fs.mkdir(targetDir, { recursive: true });
const existingPath = path.join(targetDir, 'test-skill');
await fs.mkdir(existingPath);
const skills = await linkSkill(
mockSkillSourceDir,
'workspace',
() => {},
);
expect(skills.length).toBe(1);
const skills = await linkSkill(mockSkillSourceDir, 'workspace', () => {});
expect(skills.length).toBe(1);
const stats = await fs.lstat(existingPath);
expect(stats.isSymbolicLink()).toBe(true);
},
);
const stats = await fs.lstat(existingPath);
expect(stats.isSymbolicLink()).toBe(true);
});
it('should abort linking if consent is rejected', async () => {
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
@@ -237,39 +220,40 @@ describe('skillUtils', () => {
expect(result).toBeNull();
});
itif(process.platform !== 'win32')(
'should successfully uninstall a skill even if its name was updated after linking',
async () => {
// 1. Create source skill
const sourceDir = path.join(tempDir, 'source-skill');
await fs.mkdir(sourceDir, { recursive: true });
const skillMdPath = path.join(sourceDir, 'SKILL.md');
await fs.writeFile(
skillMdPath,
'---\nname: original-name\ndescription: test\n---\nbody',
);
it('should successfully uninstall a skill even if its name was updated after linking', async () => {
// 1. Create source skill
const sourceDir = path.join(tempDir, 'source-skill');
await fs.mkdir(sourceDir, { recursive: true });
const skillMdPath = path.join(sourceDir, 'SKILL.md');
await fs.writeFile(
skillMdPath,
'---\nname: original-name\ndescription: test\n---\nbody',
);
// 2. Link it
const skillsDir = path.join(tempDir, '.gemini/skills');
await fs.mkdir(skillsDir, { recursive: true });
const destPath = path.join(skillsDir, 'original-name');
await fs.symlink(sourceDir, destPath, 'dir');
// 2. Link it
const skillsDir = path.join(tempDir, '.gemini/skills');
await fs.mkdir(skillsDir, { recursive: true });
const destPath = path.join(skillsDir, 'original-name');
await fs.symlink(
sourceDir,
destPath,
process.platform === 'win32' ? 'junction' : 'dir',
);
// 3. Update name in source
await fs.writeFile(
skillMdPath,
'---\nname: updated-name\ndescription: test\n---\nbody',
);
// 3. Update name in source
await fs.writeFile(
skillMdPath,
'---\nname: updated-name\ndescription: test\n---\nbody',
);
// 4. Uninstall by NEW name (this is the bug fix)
const result = await uninstallSkill('updated-name', 'user');
expect(result).not.toBeNull();
expect(result?.location).toBe(destPath);
// 4. Uninstall by NEW name (this is the bug fix)
const result = await uninstallSkill('updated-name', 'user');
expect(result).not.toBeNull();
expect(result?.location).toBe(destPath);
const exists = await fs.lstat(destPath).catch(() => null);
expect(exists).toBeNull();
},
);
const exists = await fs.lstat(destPath).catch(() => null);
expect(exists).toBeNull();
});
it('should successfully uninstall a skill by directory name if metadata is missing (fallback)', async () => {
const skillsDir = path.join(tempDir, '.gemini/skills');
+7 -1
View File
@@ -248,7 +248,13 @@ export async function linkSkill(
await fs.rm(destPath, { recursive: true, force: true });
}
await fs.symlink(skillSourceDir, destPath, 'dir');
// Use 'junction' on Windows to avoid EPERM errors — junctions don't
// require elevated privileges or Developer Mode (fixes #24816)
await fs.symlink(
skillSourceDir,
destPath,
process.platform === 'win32' ? 'junction' : 'dir',
);
linkedSkills.push({ name: skillName, location: destPath });
}
+1 -1
View File
@@ -908,8 +908,8 @@ export class Config implements McpContext, AgentLoopContext {
private readonly acceptRawOutputRisk: boolean;
private readonly dynamicModelConfiguration: boolean;
private pendingIncludeDirectories: string[];
private readonly enableHooks: boolean;
private readonly enableHooksUI: boolean;
private readonly enableHooks: boolean;
private hooks: { [K in HookEventName]?: HookDefinition[] } | undefined;
private projectHooks:
+15
View File
@@ -597,6 +597,21 @@ export class GeminiChat {
);
}
if (beforeModelResult.modifiedModel) {
modelToUse = resolveModel(
beforeModelResult.modifiedModel,
useGemini3_1,
useGemini3_1FlashLite,
false,
hasAccessToPreview,
this.context.config,
);
lastModelToUse = modelToUse;
// Re-evaluate contentsToUse based on the new model's feature support
contentsToUse = supportsModernFeatures(modelToUse)
? [...contentsForPreviewModel]
: [...requestContents];
}
if (beforeModelResult.modifiedConfig) {
Object.assign(config, beforeModelResult.modifiedConfig);
}
@@ -458,6 +458,15 @@ export class HookEventHandler {
);
logHookCall(this.context.config, hookCallEvent);
// Emit structured system message event for UI display
if (result.output?.systemMessage && result.outputFormat === 'json') {
coreEvents.emitHookSystemMessage({
hookName,
eventName,
message: result.output.systemMessage,
});
}
}
// Log individual errors
+15 -2
View File
@@ -204,7 +204,11 @@ describe('HookRunner', () => {
};
it('should execute command hook successfully', async () => {
const mockOutput = { decision: 'allow', reason: 'All good' };
const mockOutput = {
decision: 'allow',
reason: 'All good',
format: 'json',
};
// Mock successful execution
mockSpawn.mockStdoutOn.mockImplementation(
@@ -623,6 +627,7 @@ describe('HookRunner', () => {
hookSpecificOutput: {
additionalContext: 'Context from hook 1',
},
format: 'json',
};
let hookCallCount = 0;
@@ -803,6 +808,7 @@ describe('HookRunner', () => {
expect(result.success).toBe(true);
expect(result.exitCode).toBe(0);
// Should convert plain text to structured output
expect(result.outputFormat).toBe('text');
expect(result.output).toEqual({
decision: 'allow',
systemMessage: invalidJson,
@@ -835,6 +841,7 @@ describe('HookRunner', () => {
);
expect(result.success).toBe(true);
expect(result.outputFormat).toBe('text');
expect(result.output).toEqual({
decision: 'allow',
systemMessage: malformedJson,
@@ -868,6 +875,7 @@ describe('HookRunner', () => {
expect(result.success).toBe(false);
expect(result.exitCode).toBe(1);
expect(result.outputFormat).toBe('text');
expect(result.output).toEqual({
decision: 'allow',
systemMessage: `Warning: ${invalidJson}`,
@@ -901,6 +909,7 @@ describe('HookRunner', () => {
expect(result.success).toBe(false);
expect(result.exitCode).toBe(2);
expect(result.outputFormat).toBe('text');
expect(result.output).toEqual({
decision: 'deny',
reason: invalidJson,
@@ -936,7 +945,11 @@ describe('HookRunner', () => {
});
it('should handle double-encoded JSON string', async () => {
const mockOutput = { decision: 'allow', reason: 'All good' };
const mockOutput = {
decision: 'allow',
reason: 'All good',
format: 'json',
};
const doubleEncodedJson = JSON.stringify(JSON.stringify(mockOutput));
mockSpawn.mockStdoutOn.mockImplementation(
+5 -1
View File
@@ -447,6 +447,7 @@ export class HookRunner {
// Parse output
let output: HookOutput | undefined;
let outputFormat: 'json' | 'text' | undefined;
const textToParse = stdout.trim() || stderr.trim();
if (textToParse) {
@@ -460,6 +461,7 @@ export class HookRunner {
if (parsed && typeof parsed === 'object') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
output = parsed as HookOutput;
outputFormat = 'json';
}
} catch {
// Not JSON, convert plain text to structured output
@@ -467,6 +469,7 @@ export class HookRunner {
textToParse,
exitCode || EXIT_CODE_SUCCESS,
);
outputFormat = 'text';
}
}
@@ -475,6 +478,7 @@ export class HookRunner {
eventName,
success: exitCode === EXIT_CODE_SUCCESS,
output,
outputFormat,
stdout,
stderr,
exitCode: exitCode || EXIT_CODE_SUCCESS,
@@ -523,7 +527,7 @@ export class HookRunner {
exitCode: number,
): HookOutput {
if (exitCode === EXIT_CODE_SUCCESS) {
// Success - treat as system message or additional context
// Success
return {
decision: 'allow',
systemMessage: text,
+3
View File
@@ -48,6 +48,8 @@ export interface BeforeModelHookResult {
reason?: string;
/** Synthetic response to return instead of calling the model (if blocked) */
syntheticResponse?: GenerateContentResponse;
/** Modified model override (if not blocked) */
modifiedModel?: string;
/** Modified config (if not blocked) */
modifiedConfig?: GenerateContentConfig;
/** Modified contents (if not blocked) */
@@ -292,6 +294,7 @@ export class HookSystem {
beforeModelOutput.applyLLMRequestModifications(llmRequest);
return {
blocked: false,
modifiedModel: modifiedRequest?.model,
modifiedConfig: modifiedRequest?.config,
modifiedContents: modifiedRequest?.contents,
};
+2
View File
@@ -734,6 +734,8 @@ export interface HookExecutionResult {
exitCode?: number;
duration: number;
error?: Error;
/** The format of the output provided by the hook */
outputFormat?: 'json' | 'text';
}
/**
+16
View File
@@ -109,6 +109,13 @@ export interface HookEndPayload extends HookPayload {
success: boolean;
}
/**
* Payload for the 'hook-system-message' event.
*/
export interface HookSystemMessagePayload extends HookPayload {
message: string;
}
/**
* Payload for the 'retry-attempt' event.
*/
@@ -183,6 +190,7 @@ export enum CoreEvent {
SettingsChanged = 'settings-changed',
HookStart = 'hook-start',
HookEnd = 'hook-end',
HookSystemMessage = 'hook-system-message',
AgentsRefreshed = 'agents-refreshed',
AdminSettingsChanged = 'admin-settings-changed',
RetryAttempt = 'retry-attempt',
@@ -217,6 +225,7 @@ export interface CoreEvents extends ExtensionEvents {
[CoreEvent.SettingsChanged]: never[];
[CoreEvent.HookStart]: [HookStartPayload];
[CoreEvent.HookEnd]: [HookEndPayload];
[CoreEvent.HookSystemMessage]: [HookSystemMessagePayload];
[CoreEvent.AgentsRefreshed]: never[];
[CoreEvent.AdminSettingsChanged]: never[];
[CoreEvent.RetryAttempt]: [RetryAttemptPayload];
@@ -339,6 +348,13 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
this.emit(CoreEvent.HookEnd, payload);
}
/**
* Notifies subscribers that a hook has provided a system message.
*/
emitHookSystemMessage(payload: HookSystemMessagePayload): void {
this.emit(CoreEvent.HookSystemMessage, payload);
}
/**
* Notifies subscribers that agents have been refreshed.
*/