feat(cli): first working version of user simulator

This commit is contained in:
Hadi Minooei
2026-03-19 08:57:55 -07:00
parent 483193257e
commit b8c1dfad51
2 changed files with 29 additions and 11 deletions
+16 -4
View File
@@ -12,6 +12,12 @@ import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
import { UserSimulator } from './services/UserSimulator.js';
import { registerCleanup, setupTtyCheck } from './utils/cleanup.js';
import { PassThrough } from 'node:stream';
interface RenderMetrics {
renderTime: number;
output: string;
staticOutput?: string;
}
import {
type StartupWarning,
type Config,
@@ -137,6 +143,7 @@ export async function startInteractiveUI(
const simulateUser = config.getSimulateUser();
const simulatedStdin = new PassThrough({ encoding: 'utf8' });
let lastFrame: string | undefined;
const instance = render(
process.env['DEBUG'] ? (
<React.StrictMode>
@@ -152,9 +159,10 @@ export async function startInteractiveUI(
stdin: (simulateUser ? simulatedStdin : process.stdin) as any,
exitOnCtrlC: false,
isScreenReaderEnabled: config.getScreenReader(),
onRender: ({ renderTime }: { renderTime: number }) => {
if (renderTime > SLOW_RENDER_MS) {
recordSlowRender(config, renderTime);
onRender: (metrics: RenderMetrics) => {
lastFrame = metrics.output;
if (metrics.renderTime > SLOW_RENDER_MS) {
recordSlowRender(config, metrics.renderTime);
}
profiler.reportFrameRendered();
},
@@ -186,7 +194,11 @@ export async function startInteractiveUI(
});
if (simulateUser) {
const simulator = new UserSimulator(config, instance, simulatedStdin);
const simulator = new UserSimulator(
config,
() => lastFrame,
simulatedStdin,
);
simulator.start();
registerCleanup(() => simulator.stop());
}
+13 -7
View File
@@ -3,10 +3,9 @@
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config} from '@google/gemini-cli-core';
import { debugLogger, LlmRole } from '@google/gemini-cli-core';
import type { Config } from '@google/gemini-cli-core';
import { debugLogger, LlmRole, resolveModel } from '@google/gemini-cli-core';
import type { Writable } from 'node:stream';
import type { Instance } from 'ink';
export class UserSimulator {
private isRunning = false;
@@ -16,7 +15,7 @@ export class UserSimulator {
constructor(
private readonly config: Config,
private readonly instance: Instance,
private readonly getScreen: () => string | undefined,
private readonly stdinBuffer: Writable,
) {}
@@ -42,8 +41,7 @@ export class UserSimulator {
try {
this.isProcessing = true;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any
const screen = (this.instance as any).lastFrame() as string | undefined;
const screen = this.getScreen();
if (!screen || screen === this.lastScreenContent) return;
const strippedScreen = screen.replace(
@@ -74,9 +72,17 @@ For example:
- To enter a new prompt, output the text followed by \n.
Do NOT output markdown, explanations of your thought process, or quotes. Output ONLY the raw characters to send or <WAIT>.`;
const model = resolveModel(
this.config.getModel(),
false, // useGemini3_1
false, // useCustomToolModel
this.config.getHasAccessToPreviewModel?.() ?? true,
this.config,
);
const response = await contentGenerator.generateContent(
{
model: this.config.getModel(),
model,
contents: [
{
role: 'user',