chore(cli/slashcommands): Add status enum to SlashCommandEvent telemetry (#6166)

This commit is contained in:
Richie Foreman 2025-08-13 16:37:08 -04:00 committed by GitHub
parent 61047173a8
commit 2dbd5ecdc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 256 additions and 129 deletions

View File

@ -4,18 +4,17 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
const { logSlashCommand, SlashCommandEvent } = vi.hoisted(() => ({ const { logSlashCommand } = vi.hoisted(() => ({
logSlashCommand: vi.fn(), logSlashCommand: vi.fn(),
SlashCommandEvent: vi.fn((command, subCommand) => ({ command, subCommand })),
})); }));
vi.mock('@google/gemini-cli-core', async (importOriginal) => { vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original = const original =
await importOriginal<typeof import('@google/gemini-cli-core')>(); await importOriginal<typeof import('@google/gemini-cli-core')>();
return { return {
...original, ...original,
logSlashCommand, logSlashCommand,
SlashCommandEvent,
getIdeInstaller: vi.fn().mockReturnValue(null), getIdeInstaller: vi.fn().mockReturnValue(null),
}; };
}); });
@ -25,10 +24,10 @@ const { mockProcessExit } = vi.hoisted(() => ({
})); }));
vi.mock('node:process', () => { vi.mock('node:process', () => {
const mockProcess = { const mockProcess: Partial<NodeJS.Process> = {
exit: mockProcessExit, exit: mockProcessExit,
platform: 'test-platform', platform: 'sunos',
}; } as unknown as NodeJS.Process;
return { return {
...mockProcess, ...mockProcess,
default: mockProcess, default: mockProcess,
@ -77,22 +76,28 @@ import {
ConfirmShellCommandsActionReturn, ConfirmShellCommandsActionReturn,
SlashCommand, SlashCommand,
} from '../commands/types.js'; } from '../commands/types.js';
import { Config, ToolConfirmationOutcome } from '@google/gemini-cli-core'; import { ToolConfirmationOutcome } from '@google/gemini-cli-core';
import { LoadedSettings } from '../../config/settings.js'; import { LoadedSettings } from '../../config/settings.js';
import { MessageType } from '../types.js'; import { MessageType } from '../types.js';
import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js'; import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js';
import { FileCommandLoader } from '../../services/FileCommandLoader.js'; import { FileCommandLoader } from '../../services/FileCommandLoader.js';
import { McpPromptLoader } from '../../services/McpPromptLoader.js'; import { McpPromptLoader } from '../../services/McpPromptLoader.js';
import {
SlashCommandStatus,
makeFakeConfig,
} from '@google/gemini-cli-core/index.js';
const createTestCommand = ( function createTestCommand(
overrides: Partial<SlashCommand>, overrides: Partial<SlashCommand>,
kind: CommandKind = CommandKind.BUILT_IN, kind: CommandKind = CommandKind.BUILT_IN,
): SlashCommand => ({ ): SlashCommand {
name: 'test', return {
description: 'a test command', name: 'test',
kind, description: 'a test command',
...overrides, kind,
}); ...overrides,
};
}
describe('useSlashCommandProcessor', () => { describe('useSlashCommandProcessor', () => {
const mockAddItem = vi.fn(); const mockAddItem = vi.fn();
@ -102,15 +107,7 @@ describe('useSlashCommandProcessor', () => {
const mockOpenAuthDialog = vi.fn(); const mockOpenAuthDialog = vi.fn();
const mockSetQuittingMessages = vi.fn(); const mockSetQuittingMessages = vi.fn();
const mockConfig = { const mockConfig = makeFakeConfig({});
getProjectRoot: vi.fn(() => '/mock/cwd'),
getSessionId: vi.fn(() => 'test-session'),
getGeminiClient: vi.fn(() => ({
setHistory: vi.fn().mockResolvedValue(undefined),
})),
getExtensions: vi.fn(() => []),
getIdeMode: vi.fn(() => false),
} as unknown as Config;
const mockSettings = {} as LoadedSettings; const mockSettings = {} as LoadedSettings;
@ -314,6 +311,39 @@ describe('useSlashCommandProcessor', () => {
); );
}); });
it('sets isProcessing to false if the the input is not a command', async () => {
const setMockIsProcessing = vi.fn();
const result = setupProcessorHook([], [], [], setMockIsProcessing);
await act(async () => {
await result.current.handleSlashCommand('imnotacommand');
});
expect(setMockIsProcessing).not.toHaveBeenCalled();
});
it('sets isProcessing to false if the command has an error', async () => {
const setMockIsProcessing = vi.fn();
const failCommand = createTestCommand({
name: 'fail',
action: vi.fn().mockRejectedValue(new Error('oh no!')),
});
const result = setupProcessorHook(
[failCommand],
[],
[],
setMockIsProcessing,
);
await act(async () => {
await result.current.handleSlashCommand('/fail');
});
expect(setMockIsProcessing).toHaveBeenNthCalledWith(1, true);
expect(setMockIsProcessing).toHaveBeenNthCalledWith(2, false);
});
it('should set isProcessing to true during execution and false afterwards', async () => { it('should set isProcessing to true during execution and false afterwards', async () => {
const mockSetIsProcessing = vi.fn(); const mockSetIsProcessing = vi.fn();
const command = createTestCommand({ const command = createTestCommand({
@ -329,14 +359,14 @@ describe('useSlashCommandProcessor', () => {
}); });
// It should be true immediately after starting // It should be true immediately after starting
expect(mockSetIsProcessing).toHaveBeenCalledWith(true); expect(mockSetIsProcessing).toHaveBeenNthCalledWith(1, true);
// It should not have been called with false yet // It should not have been called with false yet
expect(mockSetIsProcessing).not.toHaveBeenCalledWith(false); expect(mockSetIsProcessing).not.toHaveBeenCalledWith(false);
await executionPromise; await executionPromise;
// After the promise resolves, it should be called with false // After the promise resolves, it should be called with false
expect(mockSetIsProcessing).toHaveBeenCalledWith(false); expect(mockSetIsProcessing).toHaveBeenNthCalledWith(2, false);
expect(mockSetIsProcessing).toHaveBeenCalledTimes(2); expect(mockSetIsProcessing).toHaveBeenCalledTimes(2);
}); });
}); });
@ -884,7 +914,9 @@ describe('useSlashCommandProcessor', () => {
const loggingTestCommands: SlashCommand[] = [ const loggingTestCommands: SlashCommand[] = [
createTestCommand({ createTestCommand({
name: 'logtest', name: 'logtest',
action: mockCommandAction, action: vi
.fn()
.mockResolvedValue({ type: 'message', content: 'hello world' }),
}), }),
createTestCommand({ createTestCommand({
name: 'logwithsub', name: 'logwithsub',
@ -895,6 +927,10 @@ describe('useSlashCommandProcessor', () => {
}), }),
], ],
}), }),
createTestCommand({
name: 'fail',
action: vi.fn().mockRejectedValue(new Error('oh no!')),
}),
createTestCommand({ createTestCommand({
name: 'logalias', name: 'logalias',
altNames: ['la'], altNames: ['la'],
@ -905,7 +941,6 @@ describe('useSlashCommandProcessor', () => {
beforeEach(() => { beforeEach(() => {
mockCommandAction.mockClear(); mockCommandAction.mockClear();
vi.mocked(logSlashCommand).mockClear(); vi.mocked(logSlashCommand).mockClear();
vi.mocked(SlashCommandEvent).mockClear();
}); });
it('should log a simple slash command', async () => { it('should log a simple slash command', async () => {
@ -917,8 +952,45 @@ describe('useSlashCommandProcessor', () => {
await result.current.handleSlashCommand('/logtest'); await result.current.handleSlashCommand('/logtest');
}); });
expect(logSlashCommand).toHaveBeenCalledTimes(1); expect(logSlashCommand).toHaveBeenCalledWith(
expect(SlashCommandEvent).toHaveBeenCalledWith('logtest', undefined); mockConfig,
expect.objectContaining({
command: 'logtest',
subcommand: undefined,
status: SlashCommandStatus.SUCCESS,
}),
);
});
it('logs nothing for a bogus command', async () => {
const result = setupProcessorHook(loggingTestCommands);
await waitFor(() =>
expect(result.current.slashCommands.length).toBeGreaterThan(0),
);
await act(async () => {
await result.current.handleSlashCommand('/bogusbogusbogus');
});
expect(logSlashCommand).not.toHaveBeenCalled();
});
it('logs a failure event for a failed command', async () => {
const result = setupProcessorHook(loggingTestCommands);
await waitFor(() =>
expect(result.current.slashCommands.length).toBeGreaterThan(0),
);
await act(async () => {
await result.current.handleSlashCommand('/fail');
});
expect(logSlashCommand).toHaveBeenCalledWith(
mockConfig,
expect.objectContaining({
command: 'fail',
status: 'error',
subcommand: undefined,
}),
);
}); });
it('should log a slash command with a subcommand', async () => { it('should log a slash command with a subcommand', async () => {
@ -930,8 +1002,13 @@ describe('useSlashCommandProcessor', () => {
await result.current.handleSlashCommand('/logwithsub sub'); await result.current.handleSlashCommand('/logwithsub sub');
}); });
expect(logSlashCommand).toHaveBeenCalledTimes(1); expect(logSlashCommand).toHaveBeenCalledWith(
expect(SlashCommandEvent).toHaveBeenCalledWith('logwithsub', 'sub'); mockConfig,
expect.objectContaining({
command: 'logwithsub',
subcommand: 'sub',
}),
);
}); });
it('should log the command path when an alias is used', async () => { it('should log the command path when an alias is used', async () => {
@ -942,8 +1019,12 @@ describe('useSlashCommandProcessor', () => {
await act(async () => { await act(async () => {
await result.current.handleSlashCommand('/la'); await result.current.handleSlashCommand('/la');
}); });
expect(logSlashCommand).toHaveBeenCalledTimes(1); expect(logSlashCommand).toHaveBeenCalledWith(
expect(SlashCommandEvent).toHaveBeenCalledWith('logalias', undefined); mockConfig,
expect.objectContaining({
command: 'logalias',
}),
);
}); });
it('should not log for unknown commands', async () => { it('should not log for unknown commands', async () => {

View File

@ -14,7 +14,8 @@ import {
GitService, GitService,
Logger, Logger,
logSlashCommand, logSlashCommand,
SlashCommandEvent, makeSlashCommandEvent,
SlashCommandStatus,
ToolConfirmationOutcome, ToolConfirmationOutcome,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { useSessionStats } from '../contexts/SessionContext.js'; import { useSessionStats } from '../contexts/SessionContext.js';
@ -235,77 +236,71 @@ export const useSlashCommandProcessor = (
oneTimeShellAllowlist?: Set<string>, oneTimeShellAllowlist?: Set<string>,
overwriteConfirmed?: boolean, overwriteConfirmed?: boolean,
): Promise<SlashCommandProcessorResult | false> => { ): Promise<SlashCommandProcessorResult | false> => {
if (typeof rawQuery !== 'string') {
return false;
}
const trimmed = rawQuery.trim();
if (!trimmed.startsWith('/') && !trimmed.startsWith('?')) {
return false;
}
setIsProcessing(true); setIsProcessing(true);
try {
if (typeof rawQuery !== 'string') { const userMessageTimestamp = Date.now();
return false; addItem({ type: MessageType.USER, text: trimmed }, userMessageTimestamp);
const parts = trimmed.substring(1).trim().split(/\s+/);
const commandPath = parts.filter((p) => p); // The parts of the command, e.g., ['memory', 'add']
let currentCommands = commands;
let commandToExecute: SlashCommand | undefined;
let pathIndex = 0;
let hasError = false;
const canonicalPath: string[] = [];
for (const part of commandPath) {
// TODO: For better performance and architectural clarity, this two-pass
// search could be replaced. A more optimal approach would be to
// pre-compute a single lookup map in `CommandService.ts` that resolves
// all name and alias conflicts during the initial loading phase. The
// processor would then perform a single, fast lookup on that map.
// First pass: check for an exact match on the primary command name.
let foundCommand = currentCommands.find((cmd) => cmd.name === part);
// Second pass: if no primary name matches, check for an alias.
if (!foundCommand) {
foundCommand = currentCommands.find((cmd) =>
cmd.altNames?.includes(part),
);
} }
const trimmed = rawQuery.trim(); if (foundCommand) {
if (!trimmed.startsWith('/') && !trimmed.startsWith('?')) { commandToExecute = foundCommand;
return false; canonicalPath.push(foundCommand.name);
} pathIndex++;
if (foundCommand.subCommands) {
const userMessageTimestamp = Date.now(); currentCommands = foundCommand.subCommands;
addItem(
{ type: MessageType.USER, text: trimmed },
userMessageTimestamp,
);
const parts = trimmed.substring(1).trim().split(/\s+/);
const commandPath = parts.filter((p) => p); // The parts of the command, e.g., ['memory', 'add']
let currentCommands = commands;
let commandToExecute: SlashCommand | undefined;
let pathIndex = 0;
const canonicalPath: string[] = [];
for (const part of commandPath) {
// TODO: For better performance and architectural clarity, this two-pass
// search could be replaced. A more optimal approach would be to
// pre-compute a single lookup map in `CommandService.ts` that resolves
// all name and alias conflicts during the initial loading phase. The
// processor would then perform a single, fast lookup on that map.
// First pass: check for an exact match on the primary command name.
let foundCommand = currentCommands.find((cmd) => cmd.name === part);
// Second pass: if no primary name matches, check for an alias.
if (!foundCommand) {
foundCommand = currentCommands.find((cmd) =>
cmd.altNames?.includes(part),
);
}
if (foundCommand) {
commandToExecute = foundCommand;
canonicalPath.push(foundCommand.name);
pathIndex++;
if (foundCommand.subCommands) {
currentCommands = foundCommand.subCommands;
} else {
break;
}
} else { } else {
break; break;
} }
} else {
break;
} }
}
const resolvedCommandPath = canonicalPath;
const subcommand =
resolvedCommandPath.length > 1
? resolvedCommandPath.slice(1).join(' ')
: undefined;
try {
if (commandToExecute) { if (commandToExecute) {
const args = parts.slice(pathIndex).join(' '); const args = parts.slice(pathIndex).join(' ');
if (commandToExecute.action) { if (commandToExecute.action) {
if (config) {
const resolvedCommandPath = canonicalPath;
const event = new SlashCommandEvent(
resolvedCommandPath[0],
resolvedCommandPath.length > 1
? resolvedCommandPath.slice(1).join(' ')
: undefined,
);
logSlashCommand(config, event);
}
const fullCommandContext: CommandContext = { const fullCommandContext: CommandContext = {
...commandContext, ...commandContext,
invocation: { invocation: {
@ -327,7 +322,6 @@ export const useSlashCommandProcessor = (
]), ]),
}; };
} }
const result = await commandToExecute.action( const result = await commandToExecute.action(
fullCommandContext, fullCommandContext,
args, args,
@ -500,8 +494,18 @@ export const useSlashCommandProcessor = (
content: `Unknown command: ${trimmed}`, content: `Unknown command: ${trimmed}`,
timestamp: new Date(), timestamp: new Date(),
}); });
return { type: 'handled' }; return { type: 'handled' };
} catch (e) { } catch (e: unknown) {
hasError = true;
if (config) {
const event = makeSlashCommandEvent({
command: resolvedCommandPath[0],
subcommand,
status: SlashCommandStatus.ERROR,
});
logSlashCommand(config, event);
}
addItem( addItem(
{ {
type: MessageType.ERROR, type: MessageType.ERROR,
@ -511,6 +515,14 @@ export const useSlashCommandProcessor = (
); );
return { type: 'handled' }; return { type: 'handled' };
} finally { } finally {
if (config && resolvedCommandPath[0] && !hasError) {
const event = makeSlashCommandEvent({
command: resolvedCommandPath[0],
subcommand,
status: SlashCommandStatus.SUCCESS,
});
logSlashCommand(config, event);
}
setIsProcessing(false); setIsProcessing(false);
} }
}, },

View File

@ -15,3 +15,4 @@ export {
IdeConnectionEvent, IdeConnectionEvent,
IdeConnectionType, IdeConnectionType,
} from './src/telemetry/types.js'; } from './src/telemetry/types.js';
export { makeFakeConfig } from './src/test-utils/config.js';

View File

@ -639,6 +639,13 @@ export class ClearcutLogger {
}); });
} }
if (event.status) {
data.push({
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SLASH_COMMAND_STATUS,
value: JSON.stringify(event.status),
});
}
this.enqueueLogEvent(this.createLogEvent(slash_command_event_name, data)); this.enqueueLogEvent(this.createLogEvent(slash_command_event_name, data));
this.flushIfNeeded(); this.flushIfNeeded();
} }

View File

@ -174,6 +174,9 @@ export enum EventMetadataKey {
// Logs the subcommand of the slash command. // Logs the subcommand of the slash command.
GEMINI_CLI_SLASH_COMMAND_SUBCOMMAND = 42, GEMINI_CLI_SLASH_COMMAND_SUBCOMMAND = 42,
// Logs the status of the slash command (e.g. 'success', 'error')
GEMINI_CLI_SLASH_COMMAND_STATUS = 51,
// ========================================================================== // ==========================================================================
// Next Speaker Check Event Keys // Next Speaker Check Event Keys
// =========================================================================== // ===========================================================================

View File

@ -39,8 +39,10 @@ export {
ApiResponseEvent, ApiResponseEvent,
TelemetryEvent, TelemetryEvent,
FlashFallbackEvent, FlashFallbackEvent,
SlashCommandEvent,
KittySequenceOverflowEvent, KittySequenceOverflowEvent,
SlashCommandEvent,
makeSlashCommandEvent,
SlashCommandStatus,
} from './types.js'; } from './types.js';
export { SpanStatusCode, ValueType } from '@opentelemetry/api'; export { SpanStatusCode, ValueType } from '@opentelemetry/api';
export { SemanticAttributes } from '@opentelemetry/semantic-conventions'; export { SemanticAttributes } from '@opentelemetry/semantic-conventions';

View File

@ -14,9 +14,17 @@ import {
ToolCallDecision, ToolCallDecision,
} from './tool-call-decision.js'; } from './tool-call-decision.js';
export class StartSessionEvent { interface BaseTelemetryEvent {
'event.name': string;
/** Current timestamp in ISO 8601 format */
'event.timestamp': string;
}
type CommonFields = keyof BaseTelemetryEvent;
export class StartSessionEvent implements BaseTelemetryEvent {
'event.name': 'cli_config'; 'event.name': 'cli_config';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
model: string; model: string;
embedding_model: string; embedding_model: string;
sandbox_enabled: boolean; sandbox_enabled: boolean;
@ -60,9 +68,9 @@ export class StartSessionEvent {
} }
} }
export class EndSessionEvent { export class EndSessionEvent implements BaseTelemetryEvent {
'event.name': 'end_session'; 'event.name': 'end_session';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
session_id?: string; session_id?: string;
constructor(config?: Config) { constructor(config?: Config) {
@ -72,9 +80,9 @@ export class EndSessionEvent {
} }
} }
export class UserPromptEvent { export class UserPromptEvent implements BaseTelemetryEvent {
'event.name': 'user_prompt'; 'event.name': 'user_prompt';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
prompt_length: number; prompt_length: number;
prompt_id: string; prompt_id: string;
auth_type?: string; auth_type?: string;
@ -95,9 +103,9 @@ export class UserPromptEvent {
} }
} }
export class ToolCallEvent { export class ToolCallEvent implements BaseTelemetryEvent {
'event.name': 'tool_call'; 'event.name': 'tool_call';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
function_name: string; function_name: string;
function_args: Record<string, unknown>; function_args: Record<string, unknown>;
duration_ms: number; duration_ms: number;
@ -142,9 +150,9 @@ export class ToolCallEvent {
} }
} }
export class ApiRequestEvent { export class ApiRequestEvent implements BaseTelemetryEvent {
'event.name': 'api_request'; 'event.name': 'api_request';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
model: string; model: string;
prompt_id: string; prompt_id: string;
request_text?: string; request_text?: string;
@ -158,9 +166,9 @@ export class ApiRequestEvent {
} }
} }
export class ApiErrorEvent { export class ApiErrorEvent implements BaseTelemetryEvent {
'event.name': 'api_error'; 'event.name': 'api_error';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
model: string; model: string;
error: string; error: string;
error_type?: string; error_type?: string;
@ -190,9 +198,9 @@ export class ApiErrorEvent {
} }
} }
export class ApiResponseEvent { export class ApiResponseEvent implements BaseTelemetryEvent {
'event.name': 'api_response'; 'event.name': 'api_response';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
model: string; model: string;
status_code?: number | string; status_code?: number | string;
duration_ms: number; duration_ms: number;
@ -234,9 +242,9 @@ export class ApiResponseEvent {
} }
} }
export class FlashFallbackEvent { export class FlashFallbackEvent implements BaseTelemetryEvent {
'event.name': 'flash_fallback'; 'event.name': 'flash_fallback';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
auth_type: string; auth_type: string;
constructor(auth_type: string) { constructor(auth_type: string) {
@ -252,9 +260,9 @@ export enum LoopType {
LLM_DETECTED_LOOP = 'llm_detected_loop', LLM_DETECTED_LOOP = 'llm_detected_loop',
} }
export class LoopDetectedEvent { export class LoopDetectedEvent implements BaseTelemetryEvent {
'event.name': 'loop_detected'; 'event.name': 'loop_detected';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
loop_type: LoopType; loop_type: LoopType;
prompt_id: string; prompt_id: string;
@ -266,9 +274,9 @@ export class LoopDetectedEvent {
} }
} }
export class NextSpeakerCheckEvent { export class NextSpeakerCheckEvent implements BaseTelemetryEvent {
'event.name': 'next_speaker_check'; 'event.name': 'next_speaker_check';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
prompt_id: string; prompt_id: string;
finish_reason: string; finish_reason: string;
result: string; result: string;
@ -282,23 +290,36 @@ export class NextSpeakerCheckEvent {
} }
} }
export class SlashCommandEvent { export interface SlashCommandEvent extends BaseTelemetryEvent {
'event.name': 'slash_command'; 'event.name': 'slash_command';
'event.timestamp': string; // ISO 8106 'event.timestamp': string; // ISO 8106
command: string; command: string;
subcommand?: string; subcommand?: string;
status?: SlashCommandStatus;
constructor(command: string, subcommand?: string) {
this['event.name'] = 'slash_command';
this['event.timestamp'] = new Date().toISOString();
this.command = command;
this.subcommand = subcommand;
}
} }
export class MalformedJsonResponseEvent { export function makeSlashCommandEvent({
command,
subcommand,
status,
}: Omit<SlashCommandEvent, CommonFields>): SlashCommandEvent {
return {
'event.name': 'slash_command',
'event.timestamp': new Date().toISOString(),
command,
subcommand,
status,
};
}
export enum SlashCommandStatus {
SUCCESS = 'success',
ERROR = 'error',
}
export class MalformedJsonResponseEvent implements BaseTelemetryEvent {
'event.name': 'malformed_json_response'; 'event.name': 'malformed_json_response';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
model: string; model: string;
constructor(model: string) { constructor(model: string) {
@ -315,7 +336,7 @@ export enum IdeConnectionType {
export class IdeConnectionEvent { export class IdeConnectionEvent {
'event.name': 'ide_connection'; 'event.name': 'ide_connection';
'event.timestamp': string; // ISO 8601 'event.timestamp': string;
connection_type: IdeConnectionType; connection_type: IdeConnectionType;
constructor(connection_type: IdeConnectionType) { constructor(connection_type: IdeConnectionType) {
@ -350,7 +371,7 @@ export type TelemetryEvent =
| FlashFallbackEvent | FlashFallbackEvent
| LoopDetectedEvent | LoopDetectedEvent
| NextSpeakerCheckEvent | NextSpeakerCheckEvent
| SlashCommandEvent | KittySequenceOverflowEvent
| MalformedJsonResponseEvent | MalformedJsonResponseEvent
| IdeConnectionEvent | IdeConnectionEvent
| KittySequenceOverflowEvent; | SlashCommandEvent;