feat(cli): inject internal tool state into UserSimulator and enhance UI noise suppression

- Subscribe UserSimulator to MESSAGE_BUS for tool-calls-update events.

- Explicitly notify simulator when the system is blocked awaiting tool approval.

- Suppress Notifications and ToastDisplay when simulateUser is enabled.

- Suppress warn/error logs in ConsolePatcher during non-interactive simulation.

- Add 100ms post-submission settle delay for robust input injection.

- Update unit tests to verify internal state injection and reliable delays.
This commit is contained in:
Mahima Shanware
2026-04-28 18:11:05 +00:00
parent aa821442fd
commit 58f1cdb24c
5 changed files with 122 additions and 16 deletions
+35 -2
View File
@@ -3,12 +3,16 @@
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config } from '@google/gemini-cli-core';
import {
debugLogger,
LlmRole,
PREVIEW_GEMINI_FLASH_MODEL,
resolveModel,
MessageBusType,
CoreToolCallStatus,
type Config,
type ToolCall,
type ToolCallsUpdateMessage,
} from '@google/gemini-cli-core';
import type { Writable } from 'node:stream';
import * as fs from 'node:fs';
@@ -31,6 +35,9 @@ export class UserSimulator {
private knowledgeBase = '';
private editableKnowledgeFile: string | null = null;
private actionHistory: string[] = [];
private pendingToolCalls: ToolCall[] = [];
private messageBusHandler: ((msg: ToolCallsUpdateMessage) => void) | null =
null;
constructor(
private readonly config: Config,
@@ -42,6 +49,16 @@ export class UserSimulator {
if (!this.config.getSimulateUser()) {
return;
}
this.messageBusHandler = (msg: ToolCallsUpdateMessage) => {
this.pendingToolCalls = msg.toolCalls.filter(
(tc) => tc.status === CoreToolCallStatus.AwaitingApproval,
);
};
this.config
.getMessageBus()
.subscribe(MessageBusType.TOOL_CALLS_UPDATE, this.messageBusHandler);
const source = this.config.getKnowledgeSource?.();
if (source) {
if (!fs.existsSync(source)) {
@@ -66,6 +83,12 @@ export class UserSimulator {
clearInterval(this.timer);
this.timer = null;
}
if (this.messageBusHandler) {
this.config
.getMessageBus()
.unsubscribe(MessageBusType.TOOL_CALLS_UPDATE, this.messageBusHandler);
this.messageBusHandler = null;
}
debugLogger.log('User simulator stopped');
}
@@ -140,6 +163,12 @@ export class UserSimulator {
.join('\n')}\n`
: '';
const pendingToolInstruction =
this.pendingToolCalls.length > 0
? `\nINTERNAL SYSTEM STATE: The system is currently BLOCKED awaiting user approval for the following tool(s): ${this.pendingToolCalls.map((tc) => tc.request.name).join(', ')}.
Ignore any 'Responding' indicators, spinners, or timers. You MUST provide a response (e.g., 'y\\r', '2\\r') to unblock the tool execution NOW.\n`
: '';
const prompt = `You are evaluating a CLI agent by simulating a user sitting at the terminal.
Look carefully at the screen and determine the CLI's current state:
@@ -170,7 +199,7 @@ JSON FORMAT:
"used_knowledge": <true if you used the User Knowledge Base below to answer this prompt, false otherwise>,
"new_rule": "<If used_knowledge is false and action is not <WAIT> or <DONE>, formulate a single, clear, reusable one-line rule combining the question and your answer without using option numbers (e.g. 1, 2) that might change. For example: 'If asked to allow pip execution, always allow it.' or 'Automatically accept edits for snake game implementation.'>"
}
${goalInstruction}${knowledgeInstruction}${historyInstruction}
${goalInstruction}${knowledgeInstruction}${historyInstruction}${pendingToolInstruction}
Here is the current terminal screen output:
@@ -332,6 +361,10 @@ ${strippedScreen}
// while preventing UI state collisions during long simulated inputs.
await new Promise((resolve) => setTimeout(resolve, 10));
}
// Wait a bit to ensure Ink has processed the full input
await new Promise((resolve) => setTimeout(resolve, 100));
this.lastScreenContent = normalizedScreen;
} else {
debugLogger.log('[SIMULATOR] Skipping (empty response)');