mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-27 14:30:44 -07:00
feat(core): experimental in-progress steering hints (2 of 2) (#19307)
This commit is contained in:
@@ -125,6 +125,7 @@ import {
|
||||
} from '../telemetry/loggers.js';
|
||||
import { fetchAdminControls } from '../code_assist/admin/admin_controls.js';
|
||||
import { isSubpath } from '../utils/paths.js';
|
||||
import { UserHintService } from './userHintService.js';
|
||||
|
||||
export interface AccessibilitySettings {
|
||||
enableLoadingPhrases?: boolean;
|
||||
@@ -481,6 +482,7 @@ export interface ConfigParameters {
|
||||
toolOutputMasking?: Partial<ToolOutputMaskingConfig>;
|
||||
disableLLMCorrection?: boolean;
|
||||
plan?: boolean;
|
||||
modelSteering?: boolean;
|
||||
onModelChange?: (model: string) => void;
|
||||
mcpEnabled?: boolean;
|
||||
extensionsEnabled?: boolean;
|
||||
@@ -670,11 +672,13 @@ export class Config {
|
||||
private readonly experimentalJitContext: boolean;
|
||||
private readonly disableLLMCorrection: boolean;
|
||||
private readonly planEnabled: boolean;
|
||||
private readonly modelSteering: boolean;
|
||||
private contextManager?: ContextManager;
|
||||
private terminalBackground: string | undefined = undefined;
|
||||
private remoteAdminSettings: AdminControlsSettings | undefined;
|
||||
private latestApiRequest: GenerateContentParameters | undefined;
|
||||
private lastModeSwitchTime: number = Date.now();
|
||||
readonly userHintService: UserHintService;
|
||||
private approvedPlanPath: string | undefined;
|
||||
|
||||
constructor(params: ConfigParameters) {
|
||||
@@ -763,6 +767,10 @@ export class Config {
|
||||
this.adminSkillsEnabled = params.adminSkillsEnabled ?? true;
|
||||
this.modelAvailabilityService = new ModelAvailabilityService();
|
||||
this.experimentalJitContext = params.experimentalJitContext ?? false;
|
||||
this.modelSteering = params.modelSteering ?? false;
|
||||
this.userHintService = new UserHintService(() =>
|
||||
this.isModelSteeringEnabled(),
|
||||
);
|
||||
this.toolOutputMasking = {
|
||||
enabled: params.toolOutputMasking?.enabled ?? true,
|
||||
toolProtectionThreshold:
|
||||
@@ -1637,6 +1645,10 @@ export class Config {
|
||||
return this.experimentalJitContext;
|
||||
}
|
||||
|
||||
isModelSteeringEnabled(): boolean {
|
||||
return this.modelSteering;
|
||||
}
|
||||
|
||||
getToolOutputMaskingEnabled(): boolean {
|
||||
return this.toolOutputMasking.enabled;
|
||||
}
|
||||
|
||||
77
packages/core/src/config/userHintService.test.ts
Normal file
77
packages/core/src/config/userHintService.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { UserHintService } from './userHintService.js';
|
||||
|
||||
describe('UserHintService', () => {
|
||||
it('is disabled by default and ignores hints', () => {
|
||||
const service = new UserHintService(() => false);
|
||||
service.addUserHint('this hint should be ignored');
|
||||
expect(service.getUserHints()).toEqual([]);
|
||||
expect(service.getLatestHintIndex()).toBe(-1);
|
||||
});
|
||||
|
||||
it('stores trimmed hints and exposes them via indexing when enabled', () => {
|
||||
const service = new UserHintService(() => true);
|
||||
|
||||
service.addUserHint(' first hint ');
|
||||
service.addUserHint('second hint');
|
||||
service.addUserHint(' ');
|
||||
|
||||
expect(service.getUserHints()).toEqual(['first hint', 'second hint']);
|
||||
expect(service.getLatestHintIndex()).toBe(1);
|
||||
expect(service.getUserHintsAfter(-1)).toEqual([
|
||||
'first hint',
|
||||
'second hint',
|
||||
]);
|
||||
expect(service.getUserHintsAfter(0)).toEqual(['second hint']);
|
||||
expect(service.getUserHintsAfter(1)).toEqual([]);
|
||||
});
|
||||
|
||||
it('tracks the last hint timestamp', () => {
|
||||
const service = new UserHintService(() => true);
|
||||
|
||||
expect(service.getLastUserHintAt()).toBeNull();
|
||||
service.addUserHint('hint');
|
||||
|
||||
const timestamp = service.getLastUserHintAt();
|
||||
expect(timestamp).not.toBeNull();
|
||||
expect(typeof timestamp).toBe('number');
|
||||
});
|
||||
|
||||
it('notifies listeners when a hint is added', () => {
|
||||
const service = new UserHintService(() => true);
|
||||
const listener = vi.fn();
|
||||
service.onUserHint(listener);
|
||||
|
||||
service.addUserHint('new hint');
|
||||
|
||||
expect(listener).toHaveBeenCalledWith('new hint');
|
||||
});
|
||||
|
||||
it('does NOT notify listeners after they are unregistered', () => {
|
||||
const service = new UserHintService(() => true);
|
||||
const listener = vi.fn();
|
||||
service.onUserHint(listener);
|
||||
service.offUserHint(listener);
|
||||
|
||||
service.addUserHint('ignored hint');
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should clear all hints', () => {
|
||||
const service = new UserHintService(() => true);
|
||||
service.addUserHint('hint 1');
|
||||
service.addUserHint('hint 2');
|
||||
expect(service.getUserHints()).toHaveLength(2);
|
||||
|
||||
service.clear();
|
||||
expect(service.getUserHints()).toHaveLength(0);
|
||||
expect(service.getLatestHintIndex()).toBe(-1);
|
||||
});
|
||||
});
|
||||
87
packages/core/src/config/userHintService.ts
Normal file
87
packages/core/src/config/userHintService.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service for managing user steering hints during a session.
|
||||
*/
|
||||
export class UserHintService {
|
||||
private readonly userHints: Array<{ text: string; timestamp: number }> = [];
|
||||
private readonly userHintListeners: Set<(hint: string) => void> = new Set();
|
||||
|
||||
constructor(private readonly isEnabled: () => boolean) {}
|
||||
|
||||
/**
|
||||
* Adds a new steering hint from the user.
|
||||
*/
|
||||
addUserHint(hint: string): void {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
const trimmed = hint.trim();
|
||||
if (trimmed.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.userHints.push({ text: trimmed, timestamp: Date.now() });
|
||||
for (const listener of this.userHintListeners) {
|
||||
listener(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener for new user hints.
|
||||
*/
|
||||
onUserHint(listener: (hint: string) => void): void {
|
||||
this.userHintListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a listener for new user hints.
|
||||
*/
|
||||
offUserHint(listener: (hint: string) => void): void {
|
||||
this.userHintListeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all collected hints.
|
||||
*/
|
||||
getUserHints(): string[] {
|
||||
return this.userHints.map((h) => h.text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns hints added after a specific index.
|
||||
*/
|
||||
getUserHintsAfter(index: number): string[] {
|
||||
if (index < 0) {
|
||||
return this.getUserHints();
|
||||
}
|
||||
return this.userHints.slice(index + 1).map((h) => h.text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the latest hint.
|
||||
*/
|
||||
getLatestHintIndex(): number {
|
||||
return this.userHints.length - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp of the last user hint.
|
||||
*/
|
||||
getLastUserHintAt(): number | null {
|
||||
if (this.userHints.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return this.userHints[this.userHints.length - 1].timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all collected hints.
|
||||
*/
|
||||
clear(): void {
|
||||
this.userHints.length = 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user