feat(core): experimental in-progress steering hints (2 of 2) (#19307)

This commit is contained in:
joshualitt
2026-02-18 14:05:50 -08:00
committed by GitHub
parent 81c8893e05
commit 87f5dd15d6
37 changed files with 1280 additions and 48 deletions

View File

@@ -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;
}

View 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);
});
});

View 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;
}
}