mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-19 01:30:42 -07:00
feat(hooks): display hook system messages in UI (#24616)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user