mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-15 05:47:18 -07:00
Prototype self-reflection.
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
"experimental": {
|
||||
"plan": true,
|
||||
"extensionReloading": true,
|
||||
"modelSteering": true
|
||||
"modelSteering": true,
|
||||
"reflection": true
|
||||
},
|
||||
"general": {
|
||||
"devtools": true
|
||||
|
||||
@@ -776,6 +776,7 @@ export async function loadCliConfig(
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
experimentalReflection: settings.experimental?.reflection,
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
toolOutputMasking: settings.experimental?.toolOutputMasking,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
|
||||
@@ -1798,6 +1798,16 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable Just-In-Time (JIT) context loading.',
|
||||
showInDialog: false,
|
||||
},
|
||||
reflection: {
|
||||
type: 'boolean',
|
||||
label: 'Continuous Learning (Reflection)',
|
||||
category: 'Experimental',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Enable the agent to periodically reflect on the session and propose new skills or memories.',
|
||||
showInDialog: true,
|
||||
},
|
||||
useOSC52Paste: {
|
||||
type: 'boolean',
|
||||
label: 'Use OSC 52 Paste',
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { LocalAgentDefinition } from './types.js';
|
||||
import {
|
||||
GLOB_TOOL_NAME,
|
||||
GREP_TOOL_NAME,
|
||||
LS_TOOL_NAME,
|
||||
READ_FILE_TOOL_NAME,
|
||||
WRITE_FILE_TOOL_NAME,
|
||||
EDIT_TOOL_NAME,
|
||||
GET_SESSION_HISTORY_TOOL_NAME,
|
||||
} from '../tools/tool-names.js';
|
||||
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
|
||||
import { z } from 'zod';
|
||||
import type { Config } from '../config/config.js';
|
||||
|
||||
// Define a type that matches the outputConfig schema for type safety.
|
||||
const ReflectAgentReportSchema = z.object({
|
||||
SummaryOfFindings: z
|
||||
.string()
|
||||
.describe(
|
||||
'A summary of what was learned during reflection, including any new skills or memories created.',
|
||||
),
|
||||
CreatedSkills: z
|
||||
.array(z.string())
|
||||
.describe('A list of skill files created or updated.'),
|
||||
AddedMemories: z
|
||||
.array(z.string())
|
||||
.describe('A list of global memories added to GEMINI.md.'),
|
||||
});
|
||||
|
||||
/**
|
||||
* A subagent specialized in reflecting on session history and preserving
|
||||
* reusable knowledge as skills or memories.
|
||||
*/
|
||||
export const ReflectAgent = (
|
||||
_config: Config,
|
||||
): LocalAgentDefinition<typeof ReflectAgentReportSchema> => ({
|
||||
name: 'reflect_agent',
|
||||
kind: 'local',
|
||||
displayName: 'Reflect Agent',
|
||||
description: `A specialized agent that reads the current chat history, identifies reusable knowledge or workflows, and saves them as skills or memories.`,
|
||||
inputConfig: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
outputConfig: {
|
||||
outputName: 'report',
|
||||
description: 'The final reflection report as a JSON object.',
|
||||
schema: ReflectAgentReportSchema,
|
||||
},
|
||||
|
||||
processOutput: (output) => JSON.stringify(output, null, 2),
|
||||
|
||||
modelConfig: {
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
generateContentConfig: {
|
||||
temperature: 0.1,
|
||||
topP: 0.95,
|
||||
},
|
||||
},
|
||||
|
||||
runConfig: {
|
||||
maxTimeMinutes: 3,
|
||||
maxTurns: 10,
|
||||
},
|
||||
|
||||
toolConfig: {
|
||||
tools: [
|
||||
GET_SESSION_HISTORY_TOOL_NAME,
|
||||
LS_TOOL_NAME,
|
||||
READ_FILE_TOOL_NAME,
|
||||
GLOB_TOOL_NAME,
|
||||
GREP_TOOL_NAME,
|
||||
WRITE_FILE_TOOL_NAME,
|
||||
EDIT_TOOL_NAME,
|
||||
],
|
||||
},
|
||||
|
||||
promptConfig: {
|
||||
query: `Please review the current session history and save any valuable learnings as skills or memories.`,
|
||||
systemPrompt: `You are the **Reflect Agent**, a specialized AI agent responsible for continuous learning. Your purpose is to review the current session's chat history, identify high-value, reusable knowledge or workflows, and persist them.
|
||||
|
||||
## Core Directives
|
||||
1. **Retrieve History:** Your very first action MUST be to call the \`get_session_history\` tool to read what happened during this session.
|
||||
2. **Analyze & Extract:** Look for complex workflows, specific project conventions, repeated commands, or user preferences that the agent should remember for future tasks.
|
||||
3. **Persist Knowledge:**
|
||||
- **Memories:** For general, workspace-level preferences (e.g., "always use 2 spaces", "use strict typescript"), update the \`GEMINI.md\` file using \`${WRITE_FILE_TOOL_NAME}\` or \`${EDIT_TOOL_NAME}\`.
|
||||
- **Skills:** For complex, task-specific scripts or workflows, create a new skill in the \`.gemini/skills/\` directory. A skill requires a \`SKILL.md\` file containing the rules and resources.
|
||||
4. **Local Changes Only:** Only modify files in the \`.gemini/\` directory or the \`GEMINI.md\` file. Do NOT modify the main application source code.
|
||||
5. **Format Report:** Once you have saved the learnings, call the \`complete_task\` tool with a structured JSON report detailing what you found and what you saved.
|
||||
`,
|
||||
},
|
||||
});
|
||||
@@ -272,6 +272,7 @@ describe('AgentRegistry', () => {
|
||||
codebase_investigator: { enabled: false },
|
||||
cli_help: { enabled: false },
|
||||
generalist: { enabled: false },
|
||||
reflect_agent: { enabled: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { loadAgentsFromDirectory } from './agentLoader.js';
|
||||
import { CodebaseInvestigatorAgent } from './codebase-investigator.js';
|
||||
import { CliHelpAgent } from './cli-help-agent.js';
|
||||
import { GeneralistAgent } from './generalist-agent.js';
|
||||
import { ReflectAgent } from './reflect-agent.js';
|
||||
import { BrowserAgentDefinition } from './browser/browserAgentDefinition.js';
|
||||
import { A2AClientManager } from './a2a-client-manager.js';
|
||||
import { A2AAuthProviderFactory } from './auth-provider/factory.js';
|
||||
@@ -242,6 +243,7 @@ export class AgentRegistry {
|
||||
this.registerLocalAgent(CodebaseInvestigatorAgent(this.config));
|
||||
this.registerLocalAgent(CliHelpAgent(this.config));
|
||||
this.registerLocalAgent(GeneralistAgent(this.config));
|
||||
this.registerLocalAgent(ReflectAgent(this.config));
|
||||
|
||||
// Register the browser agent if enabled in settings.
|
||||
// Tools are configured dynamically at invocation time via browserAgentFactory.
|
||||
|
||||
@@ -25,6 +25,7 @@ import { GrepTool } from '../tools/grep.js';
|
||||
import { canUseRipgrep, RipGrepTool } from '../tools/ripGrep.js';
|
||||
import { GlobTool } from '../tools/glob.js';
|
||||
import { ActivateSkillTool } from '../tools/activate-skill.js';
|
||||
import { GetSessionHistoryTool } from '../tools/get-session-history.js';
|
||||
import { EditTool } from '../tools/edit.js';
|
||||
import { ShellTool } from '../tools/shell.js';
|
||||
import { WriteFileTool } from '../tools/write-file.js';
|
||||
@@ -579,6 +580,7 @@ export interface ConfigParameters {
|
||||
disabledSkills?: string[];
|
||||
adminSkillsEnabled?: boolean;
|
||||
experimentalJitContext?: boolean;
|
||||
experimentalReflection?: boolean;
|
||||
toolOutputMasking?: Partial<ToolOutputMaskingConfig>;
|
||||
disableLLMCorrection?: boolean;
|
||||
plan?: boolean;
|
||||
@@ -794,6 +796,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly adminSkillsEnabled: boolean;
|
||||
|
||||
private readonly experimentalJitContext: boolean;
|
||||
private readonly experimentalReflection: boolean;
|
||||
private readonly disableLLMCorrection: boolean;
|
||||
private readonly planEnabled: boolean;
|
||||
private readonly trackerEnabled: boolean;
|
||||
@@ -895,6 +898,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.adminSkillsEnabled = params.adminSkillsEnabled ?? true;
|
||||
this.modelAvailabilityService = new ModelAvailabilityService();
|
||||
this.experimentalJitContext = params.experimentalJitContext ?? false;
|
||||
this.experimentalReflection = params.experimentalReflection ?? false;
|
||||
this.modelSteering = params.modelSteering ?? false;
|
||||
this.userHintService = new UserHintService(() =>
|
||||
this.isModelSteeringEnabled(),
|
||||
@@ -1934,6 +1938,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.experimentalJitContext;
|
||||
}
|
||||
|
||||
isReflectionEnabled(): boolean {
|
||||
return this.experimentalReflection;
|
||||
}
|
||||
|
||||
isModelSteeringEnabled(): boolean {
|
||||
return this.modelSteering;
|
||||
}
|
||||
@@ -2847,6 +2855,9 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
maybeRegister(ActivateSkillTool, () =>
|
||||
registry.registerTool(new ActivateSkillTool(this, this._messageBus)),
|
||||
);
|
||||
maybeRegister(GetSessionHistoryTool, () =>
|
||||
registry.registerTool(new GetSessionHistoryTool(this, this._messageBus)),
|
||||
);
|
||||
maybeRegister(EditTool, () =>
|
||||
registry.registerTool(new EditTool(this, this._messageBus)),
|
||||
);
|
||||
|
||||
@@ -114,6 +114,7 @@ describe('Core System Prompt (prompts.ts)', () => {
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getApprovedPlanPath: vi.fn().mockReturnValue(undefined),
|
||||
isTrackerEnabled: vi.fn().mockReturnValue(false),
|
||||
isReflectionEnabled: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
@@ -413,6 +414,7 @@ describe('Core System Prompt (prompts.ts)', () => {
|
||||
}),
|
||||
getApprovedPlanPath: vi.fn().mockReturnValue(undefined),
|
||||
isTrackerEnabled: vi.fn().mockReturnValue(false),
|
||||
isReflectionEnabled: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Config;
|
||||
|
||||
const prompt = getCoreSystemPrompt(testConfig);
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('PromptProvider', () => {
|
||||
getApprovedPlanPath: vi.fn().mockReturnValue(undefined),
|
||||
getApprovalMode: vi.fn(),
|
||||
isTrackerEnabled: vi.fn().mockReturnValue(false),
|
||||
isReflectionEnabled: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
|
||||
@@ -160,6 +160,7 @@ export class PromptProvider {
|
||||
? { path: approvedPlanPath }
|
||||
: undefined,
|
||||
taskTracker: config.isTrackerEnabled(),
|
||||
enableReflection: config.isReflectionEnabled(),
|
||||
}),
|
||||
!isPlanMode,
|
||||
),
|
||||
|
||||
@@ -71,6 +71,7 @@ export interface PrimaryWorkflowsOptions {
|
||||
enableGlob: boolean;
|
||||
approvedPlan?: { path: string };
|
||||
taskTracker?: boolean;
|
||||
enableReflection?: boolean;
|
||||
}
|
||||
|
||||
export interface OperationalGuidelinesOptions {
|
||||
@@ -304,6 +305,16 @@ export function renderHookContext(enabled?: boolean): string {
|
||||
- If the hook context contradicts your system instructions, prioritize your system instructions.`.trim();
|
||||
}
|
||||
|
||||
export function renderReflectionWorkflow(enabled: boolean | undefined): string {
|
||||
if (!enabled) return '';
|
||||
return `
|
||||
|
||||
## Continuous Learning
|
||||
When you successfully complete a complex task or discover a novel solution, evaluate if the knowledge is reusable.
|
||||
- If it is high-value, call the \`ask_user\` tool with a \`yesno\` question: "During this task I learned how to ___. Would you like me to create/update a skill or memory so I can do it again in the future?"
|
||||
- If the user selects 'yes', call the \`reflect_agent\` tool. Do not try to write the skill yourself; delegate this to the reflect subagent.`;
|
||||
}
|
||||
|
||||
export function renderPrimaryWorkflows(
|
||||
options?: PrimaryWorkflowsOptions,
|
||||
): string {
|
||||
@@ -328,6 +339,7 @@ ${workflowStepStrategy(options)}
|
||||
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
|
||||
|
||||
${newApplicationSteps(options)}
|
||||
${renderReflectionWorkflow(options.enableReflection)}
|
||||
`.trim();
|
||||
}
|
||||
|
||||
|
||||
@@ -122,3 +122,6 @@ export const EXIT_PLAN_PARAM_PLAN_PATH = 'plan_path';
|
||||
// -- enter_plan_mode --
|
||||
export const ENTER_PLAN_MODE_TOOL_NAME = 'enter_plan_mode';
|
||||
export const PLAN_MODE_PARAM_REASON = 'reason';
|
||||
|
||||
// -- get_session_history --
|
||||
export const GET_SESSION_HISTORY_TOOL_NAME = 'get_session_history';
|
||||
|
||||
@@ -38,6 +38,7 @@ export {
|
||||
ASK_USER_TOOL_NAME,
|
||||
EXIT_PLAN_MODE_TOOL_NAME,
|
||||
ENTER_PLAN_MODE_TOOL_NAME,
|
||||
GET_SESSION_HISTORY_TOOL_NAME,
|
||||
// Shared parameter names
|
||||
PARAM_FILE_PATH,
|
||||
PARAM_DIR_PATH,
|
||||
@@ -221,6 +222,13 @@ export const ENTER_PLAN_MODE_DEFINITION: ToolDefinition = {
|
||||
overrides: (modelId) => getToolSet(modelId).enter_plan_mode,
|
||||
};
|
||||
|
||||
export const GET_SESSION_HISTORY_DEFINITION: ToolDefinition = {
|
||||
get base() {
|
||||
return DEFAULT_LEGACY_SET.get_session_history;
|
||||
},
|
||||
overrides: (modelId) => getToolSet(modelId).get_session_history,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// DYNAMIC TOOL DEFINITIONS (LEGACY EXPORTS)
|
||||
// ============================================================================
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
GET_INTERNAL_DOCS_TOOL_NAME,
|
||||
ASK_USER_TOOL_NAME,
|
||||
ENTER_PLAN_MODE_TOOL_NAME,
|
||||
GET_SESSION_HISTORY_TOOL_NAME,
|
||||
// Shared parameter names
|
||||
PARAM_FILE_PATH,
|
||||
PARAM_DIR_PATH,
|
||||
@@ -732,6 +733,15 @@ The agent did not use the todo list because this task could be completed by a ti
|
||||
},
|
||||
},
|
||||
|
||||
get_session_history: {
|
||||
name: GET_SESSION_HISTORY_TOOL_NAME,
|
||||
description: 'Retrieves the complete chat history of the current session.',
|
||||
parametersJsonSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
|
||||
exit_plan_mode: (plansDir) => getExitPlanModeDeclaration(plansDir),
|
||||
activate_skill: (skillNames) => getActivateSkillDeclaration(skillNames),
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
GET_INTERNAL_DOCS_TOOL_NAME,
|
||||
ASK_USER_TOOL_NAME,
|
||||
ENTER_PLAN_MODE_TOOL_NAME,
|
||||
GET_SESSION_HISTORY_TOOL_NAME,
|
||||
// Shared parameter names
|
||||
PARAM_FILE_PATH,
|
||||
PARAM_DIR_PATH,
|
||||
@@ -707,6 +708,15 @@ The agent did not use the todo list because this task could be completed by a ti
|
||||
},
|
||||
},
|
||||
|
||||
get_session_history: {
|
||||
name: GET_SESSION_HISTORY_TOOL_NAME,
|
||||
description: 'Retrieves the complete chat history of the current session.',
|
||||
parametersJsonSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
|
||||
exit_plan_mode: (plansDir) => getExitPlanModeDeclaration(plansDir),
|
||||
activate_skill: (skillNames) => getActivateSkillDeclaration(skillNames),
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface CoreToolSet {
|
||||
get_internal_docs: FunctionDeclaration;
|
||||
ask_user: FunctionDeclaration;
|
||||
enter_plan_mode: FunctionDeclaration;
|
||||
get_session_history: FunctionDeclaration;
|
||||
exit_plan_mode: (plansDir: string) => FunctionDeclaration;
|
||||
activate_skill: (skillNames: string[]) => FunctionDeclaration;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
BaseDeclarativeTool,
|
||||
BaseToolInvocation,
|
||||
Kind,
|
||||
type ToolResult,
|
||||
} from './tools.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import { GET_SESSION_HISTORY_TOOL_NAME } from './tool-names.js';
|
||||
import { GET_SESSION_HISTORY_DEFINITION } from './definitions/coreTools.js';
|
||||
import { resolveToolDeclaration } from './definitions/resolver.js';
|
||||
|
||||
class GetSessionHistoryInvocation extends BaseToolInvocation<
|
||||
Record<string, never>,
|
||||
ToolResult
|
||||
> {
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
params: Record<string, never>,
|
||||
messageBus: MessageBus,
|
||||
toolName?: string,
|
||||
displayName?: string,
|
||||
) {
|
||||
super(params, messageBus, toolName, displayName);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Retrieving current session chat history';
|
||||
}
|
||||
|
||||
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
||||
const client = this.config.getGeminiClient();
|
||||
if (!client) {
|
||||
throw new Error('GeminiClient not initialized.');
|
||||
}
|
||||
|
||||
const history = client.getHistory();
|
||||
let historyText = '';
|
||||
|
||||
for (const turn of history) {
|
||||
historyText += `\n--- Role: ${turn.role} ---\n`;
|
||||
if (turn.parts) {
|
||||
for (const part of turn.parts) {
|
||||
if (part.text) {
|
||||
historyText += `${part.text}\n`;
|
||||
} else if (part.functionCall) {
|
||||
historyText += `[Tool Call: ${part.functionCall.name} with args: ${JSON.stringify(part.functionCall.args)}]\n`;
|
||||
} else if (part.functionResponse) {
|
||||
// Include function response safely
|
||||
const responseText = JSON.stringify(part.functionResponse.response);
|
||||
historyText += `[Tool Response: ${part.functionResponse.name} - ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}]\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
llmContent: historyText || 'No history found.',
|
||||
returnDisplay: 'Successfully retrieved session history.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class GetSessionHistoryTool extends BaseDeclarativeTool<
|
||||
Record<string, never>,
|
||||
ToolResult
|
||||
> {
|
||||
static readonly Name = GET_SESSION_HISTORY_TOOL_NAME;
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
messageBus: MessageBus,
|
||||
) {
|
||||
super(
|
||||
GetSessionHistoryTool.Name,
|
||||
'GetSessionHistory',
|
||||
GET_SESSION_HISTORY_DEFINITION.base.description!,
|
||||
Kind.Think,
|
||||
GET_SESSION_HISTORY_DEFINITION.base.parametersJsonSchema,
|
||||
messageBus,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
protected createInvocation(
|
||||
params: Record<string, never>,
|
||||
messageBus: MessageBus,
|
||||
toolName?: string,
|
||||
displayName?: string,
|
||||
) {
|
||||
return new GetSessionHistoryInvocation(
|
||||
this.config,
|
||||
params,
|
||||
messageBus,
|
||||
toolName ?? this.name,
|
||||
displayName ?? this.displayName,
|
||||
);
|
||||
}
|
||||
|
||||
override getSchema(modelId?: string) {
|
||||
return resolveToolDeclaration(GET_SESSION_HISTORY_DEFINITION, modelId);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
ASK_USER_TOOL_NAME,
|
||||
EXIT_PLAN_MODE_TOOL_NAME,
|
||||
ENTER_PLAN_MODE_TOOL_NAME,
|
||||
GET_SESSION_HISTORY_TOOL_NAME,
|
||||
// Shared parameter names
|
||||
PARAM_FILE_PATH,
|
||||
PARAM_DIR_PATH,
|
||||
@@ -95,6 +96,7 @@ export {
|
||||
ASK_USER_TOOL_NAME,
|
||||
EXIT_PLAN_MODE_TOOL_NAME,
|
||||
ENTER_PLAN_MODE_TOOL_NAME,
|
||||
GET_SESSION_HISTORY_TOOL_NAME,
|
||||
// Shared parameter names
|
||||
PARAM_FILE_PATH,
|
||||
PARAM_DIR_PATH,
|
||||
|
||||
@@ -472,6 +472,9 @@ export class ToolRegistry {
|
||||
) ?? new Set([]);
|
||||
const activeTools: AnyDeclarativeTool[] = [];
|
||||
for (const tool of this.allKnownTools.values()) {
|
||||
if (tool.name === 'get_session_history') {
|
||||
continue;
|
||||
}
|
||||
if (this.isActiveTool(tool, excludedTools)) {
|
||||
activeTools.push(tool);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user