Compare commits

..

19 Commits

Author SHA1 Message Date
Taylor Mullen 1a3741d2aa feat: implement Open Plugins agents support
- Add pluginRoot to AgentDefinition metadata
- Implement ${PLUGIN_ROOT} expansion in markdownToAgentDefinition
- Automatically discover and namespace agents in Open Plugins
- Update ExtensionManager to pass plugin root during agent loading
- Display sub-agents in extensions list output

Fixes https://github.com/google-gemini/maintainers-gemini-cli/issues/1594
2026-03-24 11:48:55 -07:00
Taylor Mullen f7413564ac fix(cli): clean up plugin.ts and fix MCP expansion test build
- Remove stray conflict markers in plugin.ts
- Add missing MCPServerConfig import and type assertions in plugin.ts
- Fix missing sanitizationConfig property in mcp-plugin-expansion.test.ts

Part of https://github.com/google-gemini/maintainers-gemini-cli/issues/1595
2026-03-24 11:45:50 -07:00
Taylor Mullen 42344c2666 feat(cli): support Open Plugins MCP servers
- Implement discovery of .mcp.json at the plugin root
- Support explicit mcpServers path or record in plugin.json
- Add variable expansion for PLUGIN_ROOT in command, args, env, and cwd
- Align MCP server naming with the pluginName:mcpServerName format
- Ensure MCP tool names use standard mcp_ prefix for API compatibility
- Enable settings and mcpServers for Open Plugins
- Refactor MCP server resolution into dedicated method
- Improve type safety in tests and configuration loading

Fixes https://github.com/google-gemini/maintainers-gemini-cli/issues/1595
2026-03-24 11:45:49 -07:00
Taylor Mullen c64c08c774 feat(cli): support skill discovery for Open Plugins
- Updated skillLoader to support discovery in skills/ subdirectories
- Implemented convention-based skill discovery for Open Plugins
- Enforced namespacing for plugin skills (plugin:skill-name)
- Refactored skill resolution into resolvePluginSkills for better maintainability
- Added comprehensive tests for Open Plugin skill discovery

Fixes https://github.com/google-gemini/maintainers-gemini-cli/issues/1592
2026-03-24 10:28:44 -07:00
Taylor Mullen 258490b19c refactor(cli): further trim open-plugin schema and core extension interface 2026-03-24 10:18:25 -07:00
Taylor Mullen afb8727bcc fix(cli): update extension name validation test to match new error message 2026-03-24 09:32:32 -07:00
Taylor Mullen ce8315df63 chore(test): add cleanup to open-plugin-discovery tests 2026-03-23 17:07:10 -07:00
Taylor Mullen 77a6c7e577 fix(cli): pass workspaceDir to createOpenPlugin to ensure correct variable hydration 2026-03-23 16:17:15 -07:00
Taylor Mullen 2da2f28b20 feat(cli): support Open Plugin (plugin.json) manifest standard
Fixes https://github.com/google-gemini/maintainers-gemini-cli/issues/1597
2026-03-23 16:05:19 -07:00
N. Taylor Mullen 42a673a52c feat(cli): always prefix extension skills with colon separator (#23566) 2026-03-23 20:02:40 +00:00
Abhi 153f2630b9 refactor(core): update production type imports from coreToolScheduler (#23498) 2026-03-23 19:48:38 +00:00
Adam Weidman d5b78dbeea fix(core): prevent redundant remote agent loading on model switch (#23576) 2026-03-23 19:45:59 +00:00
Dev Randalpura 139ef0d5bd fix(ui): make tool confirmations take up entire terminal height (#22366)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-23 19:42:30 +00:00
Abhi b2d6dc4e32 chore(config): disable agents by default (#23546) 2026-03-23 19:24:16 +00:00
Abhi ac95282758 test(core): migrate hook tests to scheduler (#23496) 2026-03-23 19:21:49 +00:00
Adam Weidman efeb9f7e7b fix(core): harden AgentSession replay semantics (#23548) 2026-03-23 18:43:38 +00:00
ruomeng 5a65610fa6 fix(plan): reserve minimum height for selection list in AskUserDialog (#23280) 2026-03-23 18:27:08 +00:00
Keith Guerin 447a854ad9 feat(cli): implement full "GEMINI CLI" logo for logged-out state (#22412) 2026-03-23 18:05:00 +00:00
matt korwel b58d79c517 fix(cli): prevent terminal escape sequences from leaking on exit (#22682)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-23 18:01:12 +00:00
88 changed files with 5221 additions and 752 deletions
+6 -2
View File
@@ -303,7 +303,8 @@ jobs:
- 'merge_queue_skipper'
- 'parse_run_context'
runs-on: 'gemini-cli-ubuntu-16-core'
if: 'false'
if: |
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
@@ -343,6 +344,7 @@ jobs:
- 'e2e_linux'
- 'e2e_mac'
- 'e2e_windows'
- 'evals'
- 'merge_queue_skipper'
runs-on: 'gemini-cli-ubuntu-16-core'
steps:
@@ -350,7 +352,8 @@ jobs:
run: |
if [[ ${NEEDS_E2E_LINUX_RESULT} != 'success' || \
${NEEDS_E2E_MAC_RESULT} != 'success' || \
${NEEDS_E2E_WINDOWS_RESULT} != 'success' ]]; then
${NEEDS_E2E_WINDOWS_RESULT} != 'success' || \
${NEEDS_EVALS_RESULT} != 'success' ]]; then
echo "One or more E2E jobs failed."
exit 1
fi
@@ -359,6 +362,7 @@ jobs:
NEEDS_E2E_LINUX_RESULT: '${{ needs.e2e_linux.result }}'
NEEDS_E2E_MAC_RESULT: '${{ needs.e2e_mac.result }}'
NEEDS_E2E_WINDOWS_RESULT: '${{ needs.e2e_windows.result }}'
NEEDS_EVALS_RESULT: '${{ needs.evals.result }}'
set_workflow_status:
runs-on: 'gemini-cli-ubuntu-16-core'
+1 -1
View File
@@ -1535,7 +1535,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`experimental.enableAgents`** (boolean):
- **Description:** Enable local and remote subagents.
- **Default:** `true`
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.worktrees`** (boolean):
+6
View File
@@ -63,6 +63,9 @@ describe.skipIf(!chromeAvailable)('browser-policy', () => {
rig.setup('browser-policy-skip-confirmation', {
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
settings: {
experimental: {
enableAgents: true,
},
agents: {
overrides: {
browser_agent: {
@@ -180,6 +183,9 @@ priority = 200
rig.setup('browser-session-warning', {
fakeResponsesPath: join(__dirname, 'browser-agent.cleanup.responses'),
settings: {
experimental: {
enableAgents: true,
},
general: {
enableAutoUpdateNotification: false,
},
@@ -0,0 +1,74 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
const pluginJson = (name: string) => `{
"name": "${name}",
"version": "1.0.0",
"description": "A test open plugin"
}`;
const agentMarkdown = (name: string, description: string) => `---
name: ${name}
description: ${description}
mcp_servers:
test-server:
command: node
args: ["\${PLUGIN_ROOT}/server.js"]
---
You are ${name}. My root is \${PLUGIN_ROOT}.`;
describe('Open Plugin agents', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => await rig.cleanup());
it('discovers, namespaces, and expands PLUGIN_ROOT for Open Plugin agents', async () => {
rig.setup('open-plugin agents test');
const pluginDir = join(rig.testDir!, 'test-plugin');
mkdirSync(pluginDir, { recursive: true });
mkdirSync(join(pluginDir, 'agents'), { recursive: true });
const pluginName = 'test-plugin';
writeFileSync(join(pluginDir, 'plugin.json'), pluginJson(pluginName));
writeFileSync(
join(pluginDir, 'agents', 'researcher.md'),
agentMarkdown('researcher', 'A researcher in ${PLUGIN_ROOT}'),
);
// Install the plugin
await rig.runCommand(['extensions', 'install', pluginDir], {
stdin: 'y\n',
});
// List extensions to verify the agent is registered and expanded
const listResult = await rig.runCommand(['extensions', 'list']);
// Check namespacing
expect(listResult).toContain(`${pluginName}:researcher`);
// Check expansion in description (via toOutputString)
const installedPathMatch = listResult.match(/Path: (.*)/);
const installedPath = installedPathMatch
? installedPathMatch[1].trim()
: '';
expect(listResult).toContain(`A researcher in ${installedPath}`);
// Check expansion in prompt by "running" the agent or checking its internal state
// For integration test, we can try to use the agent in a non-interactive mode
// but that might be slow/complex.
// Given we verified description expansion, and core tests verified prompt expansion,
// this should be sufficient.
});
});
@@ -341,11 +341,11 @@ describe('loadConfig', () => {
);
});
it('should default enableAgents to true when not provided', async () => {
it('should default enableAgents to false when not provided', async () => {
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
enableAgents: true,
enableAgents: false,
}),
);
});
+1 -1
View File
@@ -127,7 +127,7 @@ export async function loadConfig(
interactive: !isHeadlessMode(),
enableInteractiveShell: !isHeadlessMode(),
ptyInfo: 'auto',
enableAgents: settings.experimental?.enableAgents ?? true,
enableAgents: settings.experimental?.enableAgents ?? false,
};
const fileService = new FileDiscoveryService(workspaceDir, {
+4 -1
View File
@@ -7,7 +7,10 @@
- **Shortcuts**: only define keyboard shortcuts in
`packages/cli/src/ui/key/keyBindings.ts`
- Do not implement any logic performing custom string measurement or string
truncation. Use Ink layout instead leveraging ResizeObserver as needed.
truncation. Use Ink layout instead leveraging ResizeObserver as needed. When
using `ResizeObserver`, prefer the `useCallback` ref pattern (as seen in
`MaxSizedBox.tsx`) to ensure size measurements are captured as soon as the
element is available, avoiding potential rendering timing issues.
- Avoid prop drilling when at all possible.
## Testing
@@ -87,7 +87,7 @@ describe('handleValidate', () => {
});
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Invalid extension name: "INVALID_NAME". Only letters (a-z, A-Z), numbers (0-9), and dashes (-) are allowed.',
'Invalid extension name: "INVALID_NAME". Only letters (a-z, A-Z), numbers (0-9), dashes (-), and dots (.) are allowed. Names must start and end with an alphanumeric character.',
),
);
expect(processSpy).toHaveBeenCalledWith(1);
+97 -56
View File
@@ -11,7 +11,19 @@ import chalk from 'chalk';
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
import { type MergedSettings, SettingScope } from './settings.js';
import { createHash, randomUUID } from 'node:crypto';
import { loadInstallMetadata, type ExtensionConfig } from './extension.js';
import {
loadInstallMetadata,
loadGeminiConfig,
createGeminiExtension,
type ExtensionConfig,
} from './extension.js';
import {
findManifest,
loadOpenPluginConfig,
createOpenPlugin,
type OpenPluginConfig,
OPEN_PLUGIN_NAME_REGEX,
} from './plugin.js';
import {
isWorkspaceTrusted,
loadTrustedFolders,
@@ -65,7 +77,6 @@ import { maybeRequestConsentOrFail } from './extensions/consent.js';
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
import { ExtensionStorage } from './extensions/storage.js';
import {
EXTENSIONS_CONFIG_FILENAME,
INSTALL_METADATA_FILENAME,
recursivelyHydrateStrings,
type JsonObject,
@@ -293,6 +304,10 @@ Would you like to attempt to install via "git clone" instead?`,
try {
newExtensionConfig = await this.loadExtensionConfig(localSourcePath);
if (!newExtensionConfig) {
throw new Error('Failed to load extension configuration');
}
const newExtensionName = newExtensionConfig.name;
const previousName = previousExtensionConfig?.name ?? newExtensionName;
const previous = this.getExtensions().find(
@@ -757,23 +772,59 @@ Would you like to attempt to install via "git clone" instead?`,
effectiveExtensionPath = installMetadata.source;
}
try {
let config = await this.loadExtensionConfig(effectiveExtensionPath);
const manifestInfo = findManifest(effectiveExtensionPath);
if (!manifestInfo) {
debugLogger.warn(
`Warning: Skipping extension in ${effectiveExtensionPath}: No manifest found.`,
);
return null;
}
const extensionId = getExtensionId(config, installMetadata);
try {
// Bifurcate loading based on manifest type
if (manifestInfo.type === 'open-plugin') {
const config = await loadOpenPluginConfig(
manifestInfo.path,
effectiveExtensionPath,
this.workspaceDir,
);
validateName(config.name);
const extensionId = getExtensionId(config, installMetadata);
return await createOpenPlugin(
effectiveExtensionPath,
manifestInfo.path,
this.extensionEnablementManager.isEnabled(
config.name,
this.workspaceDir,
),
extensionId,
this.workspaceDir,
installMetadata,
);
}
// Gemini CLI Extension loading path
const rawConfig = await loadGeminiConfig(
manifestInfo.path,
effectiveExtensionPath,
this.workspaceDir,
);
validateName(rawConfig.name);
const extensionId = getExtensionId(rawConfig, installMetadata);
let userSettings: Record<string, string> = {};
let workspaceSettings: Record<string, string> = {};
if (this.settings.experimental.extensionConfig) {
userSettings = await getScopedEnvContents(
config,
rawConfig,
extensionId,
ExtensionSettingScope.USER,
);
if (isWorkspaceTrusted(this.settings).isTrusted) {
workspaceSettings = await getScopedEnvContents(
config,
rawConfig,
extensionId,
ExtensionSettingScope.WORKSPACE,
this.workspaceDir,
@@ -782,7 +833,8 @@ Would you like to attempt to install via "git clone" instead?`,
}
const customEnv = { ...userSettings, ...workspaceSettings };
config = resolveEnvVarsInObject(config, customEnv);
// config is already hydrated in loadGeminiConfig, but we might need to re-hydrate with customEnv
const config = resolveEnvVarsInObject(rawConfig, customEnv);
const resolvedSettings: ResolvedExtensionSetting[] = [];
if (config.settings && this.settings.experimental.extensionConfig) {
@@ -874,6 +926,7 @@ Would you like to attempt to install via "git clone" instead?`,
const hydrationContext: VariableContext = {
extensionPath: effectiveExtensionPath,
PLUGIN_ROOT: effectiveExtensionPath,
workspacePath: this.workspaceDir,
'/': path.sep,
pathSeparator: path.sep,
@@ -944,6 +997,7 @@ Would you like to attempt to install via "git clone" instead?`,
const agentLoadResult = await loadAgentsFromDirectory(
path.join(effectiveExtensionPath, 'agents'),
effectiveExtensionPath,
);
agentLoadResult.agents = agentLoadResult.agents.map((agent) => ({
...recursivelyHydrateStrings(agent, hydrationContext),
@@ -957,30 +1011,23 @@ Would you like to attempt to install via "git clone" instead?`,
);
}
return {
name: config.name,
version: config.version,
path: effectiveExtensionPath,
contextFiles,
installMetadata,
migratedTo: config.migratedTo,
mcpServers: config.mcpServers,
excludeTools: config.excludeTools,
hooks,
isActive: this.extensionEnablementManager.isEnabled(
return createGeminiExtension(
config,
effectiveExtensionPath,
this.extensionEnablementManager.isEnabled(
config.name,
this.workspaceDir,
),
id: getExtensionId(config, installMetadata),
settings: config.settings,
extensionId,
contextFiles,
resolvedSettings,
installMetadata,
hooks,
skills,
agents: agentLoadResult.agents,
themes: config.themes,
agentLoadResult.agents,
rules,
checkers,
plan: config.plan,
};
);
} catch (e) {
debugLogger.error(
`Warning: Skipping extension in ${effectiveExtensionPath}: ${getErrorMessage(
@@ -1013,39 +1060,27 @@ Would you like to attempt to install via "git clone" instead?`,
}
async loadExtensionConfig(extensionDir: string): Promise<ExtensionConfig> {
const configFilePath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME);
if (!fs.existsSync(configFilePath)) {
throw new Error(`Configuration file not found at ${configFilePath}`);
const manifestInfo = findManifest(extensionDir);
if (!manifestInfo) {
throw new Error(`Configuration file not found in ${extensionDir}`);
}
try {
const configContent = await fs.promises.readFile(configFilePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const rawConfig = JSON.parse(configContent) as ExtensionConfig;
if (!rawConfig.name || !rawConfig.version) {
throw new Error(
`Invalid configuration in ${configFilePath}: missing ${!rawConfig.name ? '"name"' : '"version"'}`,
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const config = recursivelyHydrateStrings(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
rawConfig as unknown as JsonObject,
{
extensionPath: extensionDir,
workspacePath: this.workspaceDir,
'/': path.sep,
pathSeparator: path.sep,
},
) as unknown as ExtensionConfig;
if (manifestInfo.type === 'open-plugin') {
const config = await loadOpenPluginConfig(
manifestInfo.path,
extensionDir,
this.workspaceDir,
);
validateName(config.name);
return config;
} catch (e) {
throw new Error(
`Failed to load extension config from ${configFilePath}: ${getErrorMessage(
e,
)}`,
} else {
const config = await loadGeminiConfig(
manifestInfo.path,
extensionDir,
this.workspaceDir,
);
validateName(config.name);
return config;
}
}
@@ -1150,6 +1185,12 @@ Would you like to attempt to install via "git clone" instead?`,
output += `\n ${skill.name}: ${skill.description}`;
});
}
if (extension.agents && extension.agents.length > 0) {
output += `\n Sub-agents:`;
extension.agents.forEach((agent) => {
output += `\n ${agent.name}: ${agent.description}`;
});
}
const resolvedSettings = extension.resolvedSettings;
if (resolvedSettings && resolvedSettings.length > 0) {
output += `\n Settings:`;
@@ -1279,9 +1320,9 @@ function getContextFileNames(config: ExtensionConfig): string[] {
}
function validateName(name: string) {
if (!/^[a-zA-Z0-9-]+$/.test(name)) {
if (!OPEN_PLUGIN_NAME_REGEX.test(name) && !/^[a-zA-Z0-9-]+$/.test(name)) {
throw new Error(
`Invalid extension name: "${name}". Only letters (a-z, A-Z), numbers (0-9), and dashes (-) are allowed.`,
`Invalid extension name: "${name}". Only letters (a-z, A-Z), numbers (0-9), dashes (-), and dots (.) are allowed. Names must start and end with an alphanumeric character.`,
);
}
}
@@ -1326,7 +1367,7 @@ export async function inferInstallMetadata(
}
export function getExtensionId(
config: ExtensionConfig,
config: ExtensionConfig | OpenPluginConfig,
installMetadata?: ExtensionInstallMetadata,
): string {
// IDs are created by hashing details of the installation source in order to
+70 -10
View File
@@ -46,6 +46,7 @@ import {
INSTALL_METADATA_FILENAME,
} from './extensions/variables.js';
import { hashValue, ExtensionManager } from './extension-manager.js';
import { loadGeminiConfig, createGeminiExtension } from './extension.js';
import { ExtensionStorage } from './extensions/storage.js';
import { INSTALL_WARNING_MESSAGE } from './extensions/consent.js';
import type { ExtensionSetting } from './extensions/extensionSettings.js';
@@ -685,9 +686,7 @@ name = "yolo-checker"
expect(extensions).toHaveLength(1);
expect(extensions[0].name).toBe('good-ext');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`,
),
expect.stringContaining(`Warning: Skipping extension in ${badExtDir}:`),
);
consoleSpy.mockRestore();
@@ -717,7 +716,7 @@ name = "yolo-checker"
expect(extensions[0].name).toBe('good-ext');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
`Warning: Skipping extension in ${badExtDir}: Invalid gemini-extension.json:`,
),
);
@@ -1195,14 +1194,13 @@ name = "yolo-checker"
it('should throw an error and cleanup if gemini-extension.json is missing', async () => {
const sourceExtDir = getRealPath(path.join(tempHomeDir, 'bad-extension'));
fs.mkdirSync(sourceExtDir, { recursive: true });
const configPath = path.join(sourceExtDir, EXTENSIONS_CONFIG_FILENAME);
await expect(
extensionManager.installOrUpdateExtension({
source: sourceExtDir,
type: 'local',
}),
).rejects.toThrow(`Configuration file not found at ${configPath}`);
).rejects.toThrow(`Configuration file not found in ${sourceExtDir}`);
const targetExtDir = path.join(userExtensionsDir, 'bad-extension');
expect(fs.existsSync(targetExtDir)).toBe(false);
@@ -1219,7 +1217,7 @@ name = "yolo-checker"
source: sourceExtDir,
type: 'local',
}),
).rejects.toThrow(`Failed to load extension config from ${configPath}`);
).rejects.toThrow();
});
it('should throw an error for missing name in gemini-extension.json', async () => {
@@ -1239,9 +1237,7 @@ name = "yolo-checker"
source: sourceExtDir,
type: 'local',
}),
).rejects.toThrow(
`Invalid configuration in ${configPath}: missing "name"`,
);
).rejects.toThrow('Invalid gemini-extension.json:');
});
it('should install an extension from a git URL', async () => {
@@ -2354,3 +2350,67 @@ function isEnabled(options: { name: string; enabledForPath: string }) {
const manager = new ExtensionEnablementManager();
return manager.isEnabled(options.name, options.enabledForPath);
}
describe('extension.ts - Gemini CLI Extension Loading', () => {
let geminiTempDir: string;
let geminiManifestPath: string;
beforeEach(() => {
geminiTempDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-extension-test-'),
);
geminiManifestPath = path.join(geminiTempDir, 'gemini-extension.json');
});
afterEach(() => {
if (fs.existsSync(geminiTempDir)) {
fs.rmSync(geminiTempDir, { recursive: true, force: true });
}
});
it('should load a valid gemini-extension.json and hydrate PLUGIN_ROOT', async () => {
fs.writeFileSync(
geminiManifestPath,
JSON.stringify({
name: 'test-extension',
version: '1.0.0',
description: 'Uses root: ${PLUGIN_ROOT}',
}),
);
const config = await loadGeminiConfig(
geminiManifestPath,
geminiTempDir,
'/tmp/workspace',
);
expect(config.name).toBe('test-extension');
expect(config.version).toBe('1.0.0');
expect(config.description).toBe(`Uses root: ${geminiTempDir}`);
expect(config.manifestType).toBe('gemini');
});
it('should create a GeminiCLIExtension with all fields', () => {
const config = {
name: 'test-ext',
version: '2.0.0',
description: 'A test',
themes: [],
};
const extension = createGeminiExtension(
config,
geminiTempDir,
true,
'ext-id',
['GEMINI.md'],
[],
);
expect(extension.name).toBe('test-ext');
expect(extension.version).toBe('2.0.0');
expect(extension.isActive).toBe(true);
expect(extension.manifestType).toBe('gemini');
expect(extension.contextFiles).toEqual(['GEMINI.md']);
});
});
+126 -5
View File
@@ -4,15 +4,24 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {
MCPServerConfig,
ExtensionInstallMetadata,
CustomTheme,
import {
type MCPServerConfig,
type ExtensionInstallMetadata,
type CustomTheme,
type PolicyRule,
type SafetyCheckerRule,
type GeminiCLIExtension,
type ResolvedExtensionSetting,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { INSTALL_METADATA_FILENAME } from './extensions/variables.js';
import { z } from 'zod';
import type { ExtensionSetting } from './extensions/extensionSettings.js';
import {
INSTALL_METADATA_FILENAME,
recursivelyHydrateStrings,
type JsonObject,
} from './extensions/variables.js';
/**
* Extension definition as written to disk in gemini-extension.json files.
@@ -24,6 +33,10 @@ import type { ExtensionSetting } from './extensions/extensionSettings.js';
export interface ExtensionConfig {
name: string;
version: string;
manifestType?: 'gemini' | 'open-plugin';
description?: string;
author?: string | { name: string; email?: string; url?: string };
license?: string;
mcpServers?: Record<string, MCPServerConfig>;
contextFileName?: string | string[];
excludeTools?: string[];
@@ -48,6 +61,34 @@ export interface ExtensionConfig {
migratedTo?: string;
}
export const geminiExtensionSchema = z.object({
name: z.string().min(1),
version: z.string().min(1),
description: z.string().optional(),
author: z
.union([
z.string(),
z.object({
name: z.string(),
email: z.string().optional(),
url: z.string().optional(),
}),
])
.optional(),
license: z.string().optional(),
mcpServers: z.record(z.any()).optional(),
contextFileName: z.union([z.string(), z.array(z.string())]).optional(),
excludeTools: z.array(z.string()).optional(),
settings: z.array(z.any()).optional(),
themes: z.array(z.any()).optional(),
plan: z
.object({
directory: z.string().optional(),
})
.optional(),
migratedTo: z.string().optional(),
});
export interface ExtensionUpdateInfo {
name: string;
originalVersion: string;
@@ -67,3 +108,83 @@ export function loadInstallMetadata(
return undefined;
}
}
/**
* Loads a Gemini CLI extension manifest.
*/
export async function loadGeminiConfig(
manifestPath: string,
extensionDir: string,
workspaceDir: string,
): Promise<ExtensionConfig> {
const content = await fs.promises.readFile(manifestPath, 'utf-8');
const json = JSON.parse(content) as unknown;
const result = geminiExtensionSchema.safeParse(json);
if (!result.success) {
throw new Error(`Invalid gemini-extension.json: ${result.error.message}`);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const rawConfig = result.data as unknown as ExtensionConfig;
// Hydrate strings with basic context
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const config = recursivelyHydrateStrings(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
rawConfig as unknown as JsonObject,
{
extensionPath: extensionDir,
PLUGIN_ROOT: extensionDir,
workspacePath: workspaceDir,
'/': path.sep,
pathSeparator: path.sep,
},
) as unknown as ExtensionConfig;
config.manifestType = 'gemini';
return config;
}
/**
* Factory for creating a GeminiCLIExtension from a Gemini config.
*/
export function createGeminiExtension(
config: ExtensionConfig,
extensionDir: string,
isActive: boolean,
id: string,
contextFiles: string[],
resolvedSettings: ResolvedExtensionSetting[],
installMetadata?: ExtensionInstallMetadata,
hooks?: GeminiCLIExtension['hooks'],
skills?: GeminiCLIExtension['skills'],
agents?: GeminiCLIExtension['agents'],
rules?: PolicyRule[],
checkers?: SafetyCheckerRule[],
): GeminiCLIExtension {
return {
name: config.name,
version: config.version,
path: extensionDir,
isActive,
id,
installMetadata,
manifestType: 'gemini',
description: config.description,
author: config.author,
license: config.license,
contextFiles,
mcpServers: config.mcpServers,
excludeTools: config.excludeTools,
settings: config.settings,
resolvedSettings,
hooks,
skills,
agents,
themes: config.themes,
rules,
checkers,
plan: config.plan,
migratedTo: config.migratedTo,
};
}
@@ -25,6 +25,10 @@ export const VARIABLE_SCHEMA = {
type: 'string',
description: 'The path of the extension in the filesystem.',
},
PLUGIN_ROOT: {
type: 'string',
description: 'The root path of the plugin (alias for extensionPath).',
},
workspacePath: {
type: 'string',
description: 'The absolute path of the current workspace.',
@@ -20,6 +20,16 @@ const UNMARSHALL_KEY_IGNORE_LIST: Set<string> = new Set<string>([
export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
export const OPEN_PLUGIN_CONFIG_FILENAME = 'plugin.json';
export const HIDDEN_OPEN_PLUGIN_CONFIG_FILENAME = path.join(
'.plugin',
'plugin.json',
);
export const OPEN_PLUGIN_MCP_CONFIG_FILENAME = '.mcp.json';
export const HIDDEN_OPEN_PLUGIN_MCP_CONFIG_FILENAME = path.join(
'.plugin',
'.mcp.json',
);
export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json';
export const EXTENSION_SETTINGS_FILENAME = '.env';
@@ -0,0 +1,197 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { ExtensionManager } from './extension-manager.js';
import { createTestMergedSettings } from './settings.js';
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
const mockIntegrityManager = vi.hoisted(() => ({
verify: vi.fn().mockResolvedValue('verified'),
store: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('os', async (importOriginal) => {
const mockedOs = await importOriginal<typeof os>();
return {
...mockedOs,
homedir: mockHomedir,
};
});
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
homedir: mockHomedir,
ExtensionIntegrityManager: vi
.fn()
.mockImplementation(() => mockIntegrityManager),
};
});
describe('ExtensionManager - Open Plugin Support', () => {
let tempHomeDir: string;
let tempWorkspaceDir: string;
let userExtensionsDir: string;
let extensionManager: ExtensionManager;
beforeEach(() => {
vi.clearAllMocks();
tempHomeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
);
tempWorkspaceDir = fs.mkdtempSync(
path.join(tempHomeDir, 'gemini-cli-test-workspace-'),
);
mockHomedir.mockReturnValue(tempHomeDir);
userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
fs.mkdirSync(userExtensionsDir, { recursive: true });
extensionManager = new ExtensionManager({
settings: createTestMergedSettings(),
workspaceDir: tempWorkspaceDir,
requestConsent: vi.fn().mockResolvedValue(true),
requestSetting: null,
integrityManager: mockIntegrityManager,
});
});
afterEach(() => {
if (fs.existsSync(tempHomeDir)) {
fs.rmSync(tempHomeDir, { recursive: true, force: true });
}
});
it('should discover a plugin with plugin.json', async () => {
const pluginDir = path.join(userExtensionsDir, 'test-plugin');
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, 'plugin.json'),
JSON.stringify({
name: 'hello-world',
version: '1.0.0',
description: 'An Open Plugin test',
author: { name: 'Taylor' },
license: 'Apache-2.0',
}),
);
const extensions = await extensionManager.loadExtensions();
const plugin = extensions.find((ext) => ext.name === 'hello-world');
expect(plugin).toBeDefined();
expect(plugin?.version).toBe('1.0.0');
expect(plugin?.description).toBe('An Open Plugin test');
expect(plugin?.manifestType).toBe('open-plugin');
});
it('should discover a plugin with .plugin/plugin.json', async () => {
const pluginDir = path.join(userExtensionsDir, 'hidden-plugin-dir');
const hiddenDir = path.join(pluginDir, '.plugin');
fs.mkdirSync(hiddenDir, { recursive: true });
fs.writeFileSync(
path.join(hiddenDir, 'plugin.json'),
JSON.stringify({
name: 'hidden-plugin',
version: '2.0.0',
}),
);
const extensions = await extensionManager.loadExtensions();
const plugin = extensions.find((ext) => ext.name === 'hidden-plugin');
expect(plugin).toBeDefined();
expect(plugin?.version).toBe('2.0.0');
expect(plugin?.manifestType).toBe('open-plugin');
});
it('should support PLUGIN_ROOT variable alias in metadata', async () => {
const pluginDir = path.join(userExtensionsDir, 'var-plugin');
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, 'plugin.json'),
JSON.stringify({
name: 'var-plugin',
version: '1.0.0',
description: 'Uses root: ${PLUGIN_ROOT}',
}),
);
const extensions = await extensionManager.loadExtensions();
const plugin = extensions.find((ext) => ext.name === 'var-plugin');
expect(plugin).toBeDefined();
expect(plugin?.description).toBe(`Uses root: ${pluginDir}`);
});
it('should load skills for Open Plugins', async () => {
const pluginDir = path.join(userExtensionsDir, 'feature-plugin');
fs.mkdirSync(pluginDir, { recursive: true });
const skillsDir = path.join(pluginDir, 'skills', 'test-skill');
fs.mkdirSync(skillsDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, 'plugin.json'),
JSON.stringify({
name: 'feature-plugin',
version: '1.0.0',
}),
);
fs.writeFileSync(
path.join(skillsDir, 'SKILL.md'),
`---
name: my-skill
description: "Test description"
---
Body`,
);
const extensions = await extensionManager.loadExtensions();
const plugin = extensions.find((ext) => ext.name === 'feature-plugin');
expect(plugin).toBeDefined();
expect(plugin?.skills).toBeDefined();
expect(plugin?.skills?.[0].name).toBe('feature-plugin:my-skill');
expect(plugin?.skills?.[0].extensionName).toBe('feature-plugin');
});
it('should prioritize gemini-extension.json over plugin.json', async () => {
const pluginDir = path.join(userExtensionsDir, 'dual-manifest-plugin');
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, 'gemini-extension.json'),
JSON.stringify({
name: 'gemini-plugin',
version: '1.1.1',
}),
);
fs.writeFileSync(
path.join(pluginDir, 'plugin.json'),
JSON.stringify({
name: 'open-plugin',
version: '2.2.2',
}),
);
const extensions = await extensionManager.loadExtensions();
const plugin = extensions.find((ext) => ext.name === 'gemini-plugin');
expect(plugin).toBeDefined();
expect(plugin?.version).toBe('1.1.1');
expect(plugin?.manifestType).toBe('gemini');
const openPlugin = extensions.find((ext) => ext.name === 'open-plugin');
expect(openPlugin).toBeUndefined();
});
});
@@ -0,0 +1,133 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { ExtensionManager } from './extension-manager.js';
import { createTestMergedSettings } from './settings.js';
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
const mockIntegrityManager = vi.hoisted(() => ({
verify: vi.fn().mockResolvedValue('verified'),
store: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('os', async (importOriginal) => {
const mockedOs = await importOriginal<typeof os>();
return {
...mockedOs,
homedir: mockHomedir,
};
});
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
homedir: mockHomedir,
ExtensionIntegrityManager: vi
.fn()
.mockImplementation(() => mockIntegrityManager),
};
});
describe('Open Plugin MCP Support', () => {
let tempHomeDir: string;
let tempWorkspaceDir: string;
let userExtensionsDir: string;
let extensionManager: ExtensionManager;
beforeEach(() => {
vi.clearAllMocks();
tempHomeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-test-home-mcp-'),
);
tempWorkspaceDir = fs.mkdtempSync(
path.join(tempHomeDir, 'gemini-cli-test-workspace-'),
);
mockHomedir.mockReturnValue(tempHomeDir);
userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
fs.mkdirSync(userExtensionsDir, { recursive: true });
extensionManager = new ExtensionManager({
settings: createTestMergedSettings(),
workspaceDir: tempWorkspaceDir,
requestConsent: vi.fn().mockResolvedValue(true),
requestSetting: null,
integrityManager: mockIntegrityManager,
});
});
it('should discover MCP servers from .mcp.json', async () => {
const pluginDir = path.join(userExtensionsDir, 'mcp-plugin');
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, 'plugin.json'),
JSON.stringify({
name: 'mcp-plugin',
version: '1.0.0',
}),
);
fs.writeFileSync(
path.join(pluginDir, '.mcp.json'),
JSON.stringify({
mcpServers: {
'test-server': {
command: 'node',
args: ['server.js'],
},
},
}),
);
const extensions = await extensionManager.loadExtensions();
const plugin = extensions.find((ext) => ext.name === 'mcp-plugin');
expect(plugin).toBeDefined();
expect(plugin?.mcpServers).toBeDefined();
expect(plugin?.mcpServers?.['test-server']).toBeDefined();
expect(plugin?.mcpServers?.['test-server'].command).toBe('node');
});
it('should support explicit mcpServers path in plugin.json', async () => {
const pluginDir = path.join(userExtensionsDir, 'explicit-mcp-plugin');
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, 'plugin.json'),
JSON.stringify({
name: 'explicit-mcp-plugin',
version: '1.0.0',
mcpServers: 'custom-mcp.json',
}),
);
fs.writeFileSync(
path.join(pluginDir, 'custom-mcp.json'),
JSON.stringify({
mcpServers: {
'custom-server': {
command: 'node',
args: ['custom.js'],
},
},
}),
);
const extensions = await extensionManager.loadExtensions();
const plugin = extensions.find((ext) => ext.name === 'explicit-mcp-plugin');
expect(plugin).toBeDefined();
expect(plugin?.mcpServers).toBeDefined();
expect(plugin?.mcpServers?.['custom-server']).toBeDefined();
});
});
+321
View File
@@ -0,0 +1,321 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { z } from 'zod';
import {
loadSkillsFromDir,
loadAgentsFromDirectory,
type ExtensionInstallMetadata,
type GeminiCLIExtension,
type MCPServerConfig,
} from '@google/gemini-cli-core';
import {
EXTENSIONS_CONFIG_FILENAME,
HIDDEN_OPEN_PLUGIN_CONFIG_FILENAME,
OPEN_PLUGIN_CONFIG_FILENAME,
OPEN_PLUGIN_MCP_CONFIG_FILENAME,
HIDDEN_OPEN_PLUGIN_MCP_CONFIG_FILENAME,
recursivelyHydrateStrings,
type JsonObject,
} from './extensions/variables.js';
import type { ExtensionConfig } from './extension.js';
/**
* Open Plugin manifest (plugin.json) v1.0.0
* Based on https://open-plugins.com/plugin-builders/specification
*/
export interface OpenPluginConfig {
name: string;
version?: string;
description?: string;
author?: string | { name: string; email?: string; url?: string };
license?: string;
// Component fields (parsed but currently ignored during execution per v1 plan)
skills?: string[] | Record<string, unknown>;
agents?: string[] | Record<string, unknown>;
hooks?: string[] | Record<string, unknown>;
mcpServers?: string | string[] | Record<string, unknown>;
}
export const OPEN_PLUGIN_NAME_REGEX = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/;
export const openPluginSchema = z.object({
name: z.string().min(1).max(64).regex(OPEN_PLUGIN_NAME_REGEX),
version: z.string().optional(),
description: z.string().optional(),
author: z
.union([
z.string(),
z.object({
name: z.string(),
email: z.string().optional(),
url: z.string().optional(),
}),
])
.optional(),
license: z.string().optional(),
skills: z.union([z.array(z.string()), z.record(z.any())]).optional(),
agents: z.union([z.array(z.string()), z.record(z.any())]).optional(),
hooks: z.union([z.array(z.string()), z.record(z.any())]).optional(),
mcpServers: z
.union([z.string(), z.array(z.string()), z.record(z.any())])
.optional(),
});
export const openPluginMcpSchema = z.object({
mcpServers: z.record(z.any()),
});
export interface ManifestInfo {
type: 'gemini' | 'open-plugin';
path: string;
}
export function findManifest(extensionDir: string): ManifestInfo | undefined {
const geminiPath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME);
if (fs.existsSync(geminiPath)) {
return { type: 'gemini', path: geminiPath };
}
const openPluginPath = path.join(extensionDir, OPEN_PLUGIN_CONFIG_FILENAME);
if (fs.existsSync(openPluginPath)) {
return { type: 'open-plugin', path: openPluginPath };
}
const hiddenOpenPluginPath = path.join(
extensionDir,
HIDDEN_OPEN_PLUGIN_CONFIG_FILENAME,
);
if (fs.existsSync(hiddenOpenPluginPath)) {
return { type: 'open-plugin', path: hiddenOpenPluginPath };
}
return undefined;
}
/**
* Loads an Open Plugin manifest and maps it to ExtensionConfig.
*/
export async function loadOpenPluginConfig(
manifestPath: string,
extensionDir: string,
workspaceDir: string,
): Promise<ExtensionConfig> {
const content = await fs.promises.readFile(manifestPath, 'utf-8');
const json = JSON.parse(content) as unknown;
const result = openPluginSchema.safeParse(json);
if (!result.success) {
throw new Error(`Invalid plugin.json: ${result.error.message}`);
}
const rawConfig = result.data as OpenPluginConfig;
// Hydrate metadata fields
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const hydratedConfig = recursivelyHydrateStrings(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
rawConfig as unknown as JsonObject,
{
extensionPath: extensionDir,
PLUGIN_ROOT: extensionDir,
workspacePath: workspaceDir,
'/': path.sep,
pathSeparator: path.sep,
},
) as unknown as OpenPluginConfig;
const mcpServers = await resolveMcpServers(hydratedConfig, extensionDir);
return {
name: hydratedConfig.name,
version: hydratedConfig.version ?? '0.0.0',
manifestType: 'open-plugin',
description: hydratedConfig.description,
author: hydratedConfig.author,
license: hydratedConfig.license,
mcpServers,
};
}
/**
* Resolves MCP server configurations for an Open Plugin by checking the manifest
* and falling back to default filesystem locations.
*/
async function resolveMcpServers(
hydratedConfig: OpenPluginConfig,
extensionDir: string,
): Promise<Record<string, MCPServerConfig> | undefined> {
let mcpServers: Record<string, MCPServerConfig> | undefined;
// 1. Explicit mcpServers in plugin.json
if (hydratedConfig.mcpServers) {
if (typeof hydratedConfig.mcpServers === 'string') {
const mcpPath = path.resolve(extensionDir, hydratedConfig.mcpServers);
mcpServers = await loadMcpConfigFile(mcpPath);
} else if (Array.isArray(hydratedConfig.mcpServers)) {
const mcpServersValue = hydratedConfig.mcpServers;
if (mcpServersValue.length > 0) {
const first = mcpServersValue[0];
if (typeof first === 'string') {
// Support array of paths
mcpServers = {};
for (const p of mcpServersValue) {
const mcpPath = path.resolve(extensionDir, p);
const servers = await loadMcpConfigFile(mcpPath);
if (servers) {
Object.assign(mcpServers, servers);
}
}
}
}
} else {
// It's a Record<string, MCPServerConfig>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
mcpServers = hydratedConfig.mcpServers as Record<string, MCPServerConfig>;
}
}
// 2. Fallback to .mcp.json at plugin root if no servers found yet
if (!mcpServers) {
const mcpPath = path.join(extensionDir, OPEN_PLUGIN_MCP_CONFIG_FILENAME);
const hiddenMcpPath = path.join(
extensionDir,
HIDDEN_OPEN_PLUGIN_MCP_CONFIG_FILENAME,
);
if (fs.existsSync(mcpPath)) {
mcpServers = await loadMcpConfigFile(mcpPath);
} else if (fs.existsSync(hiddenMcpPath)) {
mcpServers = await loadMcpConfigFile(hiddenMcpPath);
}
}
return mcpServers;
}
async function loadMcpConfigFile(
mcpPath: string,
): Promise<Record<string, MCPServerConfig> | undefined> {
try {
const content = await fs.promises.readFile(mcpPath, 'utf-8');
const json = JSON.parse(content) as unknown;
const result = openPluginMcpSchema.safeParse(json);
if (result.success) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return result.data.mcpServers as Record<string, MCPServerConfig>;
}
} catch (_e) {
// Ignore errors loading fallback file
}
return undefined;
}
/**
* Creates a GeminiCLIExtension from an Open Plugin directory.
*/
export async function createOpenPlugin(
pluginDir: string,
manifestPath: string,
isActive: boolean,
id: string,
workspaceDir: string,
installMetadata?: ExtensionInstallMetadata,
): Promise<GeminiCLIExtension> {
// Use loadOpenPluginConfig to get standard mapping
const config = await loadOpenPluginConfig(
manifestPath,
pluginDir,
workspaceDir,
);
const hydrationContext = {
extensionPath: pluginDir,
PLUGIN_ROOT: pluginDir,
workspacePath: workspaceDir,
'/': path.sep,
pathSeparator: path.sep,
};
const skills = await resolvePluginSkills(
pluginDir,
config.name,
hydrationContext,
);
const agents = await resolvePluginAgents(
pluginDir,
config.name,
hydrationContext,
);
return {
name: config.name,
version: config.version,
path: pluginDir,
isActive,
id,
installMetadata,
manifestType: 'open-plugin',
description: config.description,
author: config.author,
license: config.license,
// Features partially enabled for Open Plugins
contextFiles: [],
mcpServers: config.mcpServers,
excludeTools: undefined,
settings: undefined,
resolvedSettings: undefined,
skills,
agents,
themes: undefined,
};
}
/**
* Discovers and namespaces skills for an Open Plugin.
*/
async function resolvePluginSkills(
pluginDir: string,
pluginName: string,
hydrationContext: Record<string, string>,
): Promise<GeminiCLIExtension['skills']> {
const skillsDir = path.join(pluginDir, 'skills');
const discoveredSkills = await loadSkillsFromDir(skillsDir);
if (discoveredSkills.length === 0) {
return undefined;
}
return discoveredSkills.map((skill) => ({
...recursivelyHydrateStrings(skill, hydrationContext),
name: `${pluginName}:${skill.name}`,
extensionName: pluginName,
}));
}
/**
* Discovers and namespaces agents for an Open Plugin.
*/
async function resolvePluginAgents(
pluginDir: string,
pluginName: string,
hydrationContext: Record<string, string>,
): Promise<GeminiCLIExtension['agents']> {
const agentsDir = path.join(pluginDir, 'agents');
const agentLoadResult = await loadAgentsFromDirectory(agentsDir, pluginDir);
if (agentLoadResult.agents.length === 0) {
return undefined;
}
return agentLoadResult.agents.map((agent) => ({
...recursivelyHydrateStrings(agent, hydrationContext),
name: `${pluginName}:${agent.name}`,
extensionName: pluginName,
}));
}
@@ -400,7 +400,7 @@ describe('SettingsSchema', () => {
expect(setting).toBeDefined();
expect(setting.type).toBe('boolean');
expect(setting.category).toBe('Experimental');
expect(setting.default).toBe(true);
expect(setting.default).toBe(false);
expect(setting.requiresRestart).toBe(true);
expect(setting.showInDialog).toBe(false);
expect(setting.description).toBe('Enable local and remote subagents.');
+1 -1
View File
@@ -1922,7 +1922,7 @@ const SETTINGS_SCHEMA = {
label: 'Enable Agents',
category: 'Experimental',
requiresRestart: true,
default: true,
default: false,
description: 'Enable local and remote subagents.',
showInDialog: false,
},
@@ -43,7 +43,7 @@ describe('SlashCommandResolver', () => {
]);
expect(finalCommands.map((c) => c.name)).toContain('deploy');
expect(finalCommands.map((c) => c.name)).toContain('firebase.deploy');
expect(finalCommands.map((c) => c.name)).toContain('firebase:deploy');
expect(conflicts).toHaveLength(1);
});
@@ -159,7 +159,7 @@ describe('SlashCommandResolver', () => {
it('should apply numeric suffixes when renames also conflict', () => {
const user1 = createMockCommand('deploy', CommandKind.USER_FILE);
const user2 = createMockCommand('gcp.deploy', CommandKind.USER_FILE);
const user2 = createMockCommand('gcp:deploy', CommandKind.USER_FILE);
const extension = {
...createMockCommand('deploy', CommandKind.EXTENSION_FILE),
extensionName: 'gcp',
@@ -171,7 +171,7 @@ describe('SlashCommandResolver', () => {
extension,
]);
expect(finalCommands.find((c) => c.name === 'gcp.deploy1')).toBeDefined();
expect(finalCommands.find((c) => c.name === 'gcp:deploy1')).toBeDefined();
});
it('should prefix skills with extension name when they conflict with built-in', () => {
@@ -185,7 +185,50 @@ describe('SlashCommandResolver', () => {
const names = finalCommands.map((c) => c.name);
expect(names).toContain('chat');
expect(names).toContain('google-workspace.chat');
expect(names).toContain('google-workspace:chat');
});
it('should ALWAYS prefix extension skills even if no conflict exists', () => {
const skill = {
...createMockCommand('chat', CommandKind.SKILL),
extensionName: 'google-workspace',
};
const { finalCommands } = SlashCommandResolver.resolve([skill]);
const names = finalCommands.map((c) => c.name);
expect(names).toContain('google-workspace:chat');
expect(names).not.toContain('chat');
});
it('should use numeric suffixes if prefixed skill names collide', () => {
const skill1 = {
...createMockCommand('chat', CommandKind.SKILL),
extensionName: 'google-workspace',
};
const skill2 = {
...createMockCommand('chat', CommandKind.SKILL),
extensionName: 'google-workspace',
};
const { finalCommands } = SlashCommandResolver.resolve([skill1, skill2]);
const names = finalCommands.map((c) => c.name);
expect(names).toContain('google-workspace:chat');
expect(names).toContain('google-workspace:chat1');
});
it('should NOT double-prefix when a skill name is already prefixed', () => {
const skill = {
...createMockCommand('google-workspace:chat', CommandKind.SKILL),
extensionName: 'google-workspace',
};
const { finalCommands } = SlashCommandResolver.resolve([skill]);
const names = finalCommands.map((c) => c.name);
expect(names).toContain('google-workspace:chat');
expect(names).not.toContain('google-workspace:google-workspace:chat');
});
it('should NOT prefix skills with "skill" when extension name is missing', () => {
@@ -47,7 +47,17 @@ export class SlashCommandResolver {
const originalName = cmd.name;
let finalName = originalName;
if (registry.firstEncounters.has(originalName)) {
const shouldAlwaysPrefix =
cmd.kind === CommandKind.SKILL && !!cmd.extensionName;
if (shouldAlwaysPrefix) {
finalName = this.getRenamedName(
originalName,
this.getPrefix(cmd),
registry.commandMap,
cmd.kind,
);
} else if (registry.firstEncounters.has(originalName)) {
// We've already seen a command with this name, so resolve the conflict.
finalName = this.handleConflict(cmd, registry);
} else {
@@ -93,6 +103,7 @@ export class SlashCommandResolver {
incoming.name,
this.getPrefix(incoming),
registry.commandMap,
incoming.kind,
);
this.trackConflict(
registry.conflictsMap,
@@ -132,6 +143,7 @@ export class SlashCommandResolver {
currentOwner.name,
this.getPrefix(currentOwner),
registry.commandMap,
currentOwner.kind,
);
// Update the registry: remove the old name and add the owner under the new name.
@@ -156,8 +168,20 @@ export class SlashCommandResolver {
name: string,
prefix: string | undefined,
commandMap: Map<string, SlashCommand>,
kind?: CommandKind,
): string {
const base = prefix ? `${prefix}.${name}` : name;
const isExtensionPrefix =
kind === CommandKind.SKILL || kind === CommandKind.EXTENSION_FILE;
const separator = isExtensionPrefix ? ':' : '.';
// Check if it's already correctly prefixed
const alreadyPrefixed = prefix && name.startsWith(`${prefix}${separator}`);
const base = alreadyPrefixed
? name
: prefix
? `${prefix}${separator}${name}`
: name;
let renamedName = base;
let suffix = 1;
+12 -2
View File
@@ -11,7 +11,11 @@ import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { AppContainer } from '../ui/AppContainer.js';
import { renderWithProviders, type RenderInstance } from './render.js';
import {
renderWithProviders,
type RenderInstance,
persistentStateMock,
} from './render.js';
import {
makeFakeConfig,
type Config,
@@ -180,6 +184,11 @@ export class AppRig {
}
async initialize() {
persistentStateMock.setData({
terminalSetupPromptShown: true,
tipsShown: 10,
});
this.setupEnvironment();
resetSettingsCacheForTesting();
this.settings = this.createRigSettings();
@@ -226,6 +235,8 @@ export class AppRig {
private setupEnvironment() {
// Stub environment variables to avoid interference from developer's machine
vi.stubEnv('GEMINI_CLI_HOME', this.testDir);
vi.stubEnv('TERM_PROGRAM', 'other');
vi.stubEnv('VSCODE_GIT_IPC_HANDLE', '');
if (this.options.fakeResponsesPath) {
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
MockShellExecutionService.setPassthrough(false);
@@ -291,7 +302,6 @@ export class AppRig {
const newContentGeneratorConfig = {
authType: authMethod,
proxy: gcConfig.getProxy(),
apiKey: process.env['GEMINI_API_KEY'] || 'test-api-key',
};
+1 -1
View File
@@ -665,7 +665,7 @@ export const renderWithProviders = async (
);
}
const mainAreaWidth = terminalWidth;
const mainAreaWidth = providedUiState?.mainAreaWidth ?? terminalWidth;
const finalUiState = {
...baseState,
+1 -1
View File
@@ -1419,7 +1419,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
setControlsHeight(roundedHeight);
}
}
}, [buffer, terminalWidth, terminalHeight, controlsHeight]);
}, [buffer, terminalWidth, terminalHeight, controlsHeight, isInputActive]);
// Compute available terminal height based on controls measurement
const availableTerminalHeight = Math.max(
@@ -0,0 +1,179 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { cleanup, renderWithProviders } from '../test-utils/render.js';
import { createMockSettings } from '../test-utils/settings.js';
import { App } from './App.js';
import {
CoreToolCallStatus,
ApprovalMode,
makeFakeConfig,
} from '@google/gemini-cli-core';
import { type UIState } from './contexts/UIStateContext.js';
import type { SerializableConfirmationDetails } from '@google/gemini-cli-core';
import { act } from 'react';
import { StreamingState } from './types.js';
vi.mock('ink', async (importOriginal) => {
const original = await importOriginal<typeof import('ink')>();
return {
...original,
useIsScreenReaderEnabled: vi.fn(() => false),
};
});
vi.mock('./components/GeminiSpinner.js', () => ({
GeminiSpinner: () => null,
}));
vi.mock('./components/CliSpinner.js', () => ({
CliSpinner: () => null,
}));
// Mock hooks to align with codebase style, even if App uses UIState directly
vi.mock('./hooks/useGeminiStream.js');
vi.mock('./hooks/useHistoryManager.js');
vi.mock('./hooks/useQuotaAndFallback.js');
vi.mock('./hooks/useThemeCommand.js');
vi.mock('./auth/useAuth.js');
vi.mock('./hooks/useEditorSettings.js');
vi.mock('./hooks/useSettingsCommand.js');
vi.mock('./hooks/useModelCommand.js');
vi.mock('./hooks/slashCommandProcessor.js');
vi.mock('./hooks/useConsoleMessages.js');
vi.mock('./hooks/useTerminalSize.js', () => ({
useTerminalSize: vi.fn(() => ({ columns: 100, rows: 30 })),
}));
describe('Full Terminal Tool Confirmation Snapshot', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
it('renders tool confirmation box in the frame of the entire terminal', async () => {
// Generate a large diff to warrant truncation
let largeDiff =
'--- a/packages/cli/src/ui/components/InputPrompt.tsx\n+++ b/packages/cli/src/ui/components/InputPrompt.tsx\n@@ -1,100 +1,105 @@\n';
for (let i = 1; i <= 60; i++) {
largeDiff += ` const line${i} = true;\n`;
}
largeDiff += '- return kittyProtocolSupporte...;\n';
largeDiff += '+ return kittyProtocolSupporte...;\n';
largeDiff += ' buffer: TextBuffer;\n';
largeDiff += ' onSubmit: (value: string) => void;';
const confirmationDetails: SerializableConfirmationDetails = {
type: 'edit',
title: 'Edit packages/.../InputPrompt.tsx',
fileName: 'InputPrompt.tsx',
filePath: 'packages/.../InputPrompt.tsx',
fileDiff: largeDiff,
originalContent: 'old',
newContent: 'new',
isModifying: false,
};
const toolCalls = [
{
callId: 'call-1-modify-selected',
name: 'Edit',
description:
'packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProtocolSupporte...',
status: CoreToolCallStatus.AwaitingApproval,
resultDisplay: '',
confirmationDetails,
},
];
const mockUIState = {
history: [
{
id: 1,
type: 'user',
text: 'Can you edit InputPrompt.tsx for me?',
},
],
mainAreaWidth: 99,
availableTerminalHeight: 36,
streamingState: StreamingState.WaitingForConfirmation,
constrainHeight: true,
isConfigInitialized: true,
cleanUiDetailsVisible: true,
quota: {
userTier: 'PRO',
stats: {
limits: {},
usage: {},
},
proQuotaRequest: null,
validationRequest: null,
},
pendingHistoryItems: [
{
id: 2,
type: 'tool_group',
tools: toolCalls,
},
],
showApprovalModeIndicator: ApprovalMode.DEFAULT,
sessionStats: {
lastPromptTokenCount: 175400,
contextPercentage: 3,
},
buffer: { text: '' },
messageQueue: [],
activeHooks: [],
contextFileNames: [],
rootUiRef: { current: null },
} as unknown as UIState;
const mockConfig = makeFakeConfig();
mockConfig.getUseAlternateBuffer = () => true;
mockConfig.isTrustedFolder = () => true;
mockConfig.getDisableAlwaysAllow = () => false;
mockConfig.getIdeMode = () => false;
mockConfig.getTargetDir = () => '/directory';
const { waitUntilReady, lastFrame, generateSvg, unmount } =
await renderWithProviders(<App />, {
uiState: mockUIState,
config: mockConfig,
settings: createMockSettings({
merged: {
ui: {
useAlternateBuffer: true,
theme: 'default',
showUserIdentity: false,
showShortcutsHint: false,
footer: {
hideContextPercentage: false,
hideTokens: false,
hideModel: false,
},
},
security: {
enablePermanentToolApproval: true,
},
},
}),
});
await waitUntilReady();
// Give it a moment to render
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
});
await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot();
unmount();
});
});
@@ -2,10 +2,13 @@
exports[`App > Snapshots > renders default layout correctly 1`] = `
"
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.2.3
Tips for getting started:
@@ -31,9 +34,6 @@ Tips for getting started:
@@ -47,10 +47,13 @@ exports[`App > Snapshots > renders screen reader layout correctly 1`] = `
"Notifications
Footer
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.2.3
Tips for getting started:
@@ -64,12 +67,12 @@ Composer
exports[`App > Snapshots > renders with dialogs visible 1`] = `
"
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.2.3
@@ -107,10 +110,13 @@ DialogManager
exports[`App > should render ToolConfirmationQueue along with Composer when tool is confirming and experiment is on 1`] = `
"
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.2.3
Tips for getting started:
@@ -140,9 +146,6 @@ HistoryItemDisplay
Notifications
Composer
"
@@ -0,0 +1,239 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="683" viewBox="0 0 920 683">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="683" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#ffffaf" textLength="891" lengthAdjust="spacingAndGlyphs">╭─────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#ffffaf" textLength="135" lengthAdjust="spacingAndGlyphs" font-weight="bold">Action Required</text>
<text x="882" y="19" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="882" y="36" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="53" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">?</text>
<text x="45" y="53" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs" font-weight="bold">Edit</text>
<text x="90" y="53" fill="#afafaf" textLength="774" lengthAdjust="spacingAndGlyphs">packages/.../InputPrompt.tsx: return kittyProtocolSupporte... =&gt; return kittyProto</text>
<text x="864" y="53" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs"></text>
<text x="882" y="53" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="70" fill="#333333" textLength="873" lengthAdjust="spacingAndGlyphs">─────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="882" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="87" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">46</text>
<text x="63" y="87" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="87" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line46</text>
<text x="171" y="87" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="87" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="87" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="104" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">47</text>
<text x="63" y="104" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="104" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line47</text>
<text x="171" y="104" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="104" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="104" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="121" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">48</text>
<text x="63" y="121" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="121" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line48</text>
<text x="171" y="121" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="121" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="121" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="138" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">49</text>
<text x="63" y="138" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="138" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line49</text>
<text x="171" y="138" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="138" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="138" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="155" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">50</text>
<text x="63" y="155" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="155" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line50</text>
<text x="171" y="155" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="155" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="155" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="172" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">51</text>
<text x="63" y="172" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="172" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line51</text>
<text x="171" y="172" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="172" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="172" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="189" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">52</text>
<text x="63" y="189" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="189" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line52</text>
<text x="171" y="189" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="189" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="189" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="206" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">53</text>
<text x="63" y="206" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="206" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line53</text>
<text x="171" y="206" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="206" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="206" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="223" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">54</text>
<text x="63" y="223" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="223" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line54</text>
<text x="171" y="223" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="223" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="223" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="240" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">55</text>
<text x="63" y="240" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="240" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line55</text>
<text x="171" y="240" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="240" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="240" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="257" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">56</text>
<text x="63" y="257" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="257" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line56</text>
<text x="171" y="257" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="257" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="257" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="274" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">57</text>
<text x="63" y="274" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="274" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line57</text>
<text x="171" y="274" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="274" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="274" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="291" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">58</text>
<text x="63" y="291" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="291" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line58</text>
<text x="171" y="291" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="291" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="291" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="308" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">59</text>
<text x="63" y="308" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="308" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line59</text>
<text x="171" y="308" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="308" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="308" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="325" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">60</text>
<text x="63" y="325" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
<text x="117" y="325" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line60</text>
<text x="171" y="325" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
<text x="198" y="325" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="325" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="340" width="18" height="17" fill="#5f0000" />
<text x="18" y="342" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">61</text>
<rect x="36" y="340" width="9" height="17" fill="#5f0000" />
<rect x="45" y="340" width="9" height="17" fill="#5f0000" />
<text x="45" y="342" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="340" width="9" height="17" fill="#5f0000" />
<rect x="63" y="340" width="9" height="17" fill="#5f0000" />
<rect x="72" y="340" width="54" height="17" fill="#5f0000" />
<text x="72" y="342" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
<rect x="126" y="340" width="234" height="17" fill="#5f0000" />
<text x="126" y="342" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</text>
<text x="882" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="357" width="18" height="17" fill="#005f00" />
<text x="18" y="359" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">61</text>
<rect x="36" y="357" width="9" height="17" fill="#005f00" />
<rect x="45" y="357" width="9" height="17" fill="#005f00" />
<text x="45" y="359" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="357" width="9" height="17" fill="#005f00" />
<rect x="63" y="357" width="9" height="17" fill="#005f00" />
<rect x="72" y="357" width="54" height="17" fill="#005f00" />
<text x="72" y="359" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
<rect x="126" y="357" width="234" height="17" fill="#005f00" />
<text x="126" y="359" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</text>
<text x="882" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="376" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">62</text>
<text x="63" y="376" fill="#e5e5e5" textLength="180" lengthAdjust="spacingAndGlyphs"> buffer: TextBuffer;</text>
<text x="882" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="393" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">63</text>
<text x="72" y="393" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">onSubmit</text>
<text x="144" y="393" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs">: (</text>
<text x="171" y="393" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">value</text>
<text x="216" y="393" fill="#e5e5e5" textLength="18" lengthAdjust="spacingAndGlyphs">: </text>
<text x="234" y="393" fill="#00cdcd" textLength="54" lengthAdjust="spacingAndGlyphs">string</text>
<text x="288" y="393" fill="#e5e5e5" textLength="45" lengthAdjust="spacingAndGlyphs">) =&gt; </text>
<text x="333" y="393" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">void</text>
<text x="369" y="393" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="410" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Apply this change?</text>
<text x="882" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="882" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="442" width="9" height="17" fill="#001a00" />
<text x="18" y="444" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="442" width="9" height="17" fill="#001a00" />
<rect x="36" y="442" width="18" height="17" fill="#001a00" />
<text x="36" y="444" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
<rect x="54" y="442" width="9" height="17" fill="#001a00" />
<rect x="63" y="442" width="90" height="17" fill="#001a00" />
<text x="63" y="444" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
<rect x="153" y="442" width="288" height="17" fill="#001a00" />
<text x="882" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="461" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">2.</text>
<text x="63" y="461" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Allow for this session</text>
<text x="882" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="478" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
<text x="63" y="478" fill="#ffffff" textLength="378" lengthAdjust="spacingAndGlyphs">Allow for this file in all future sessions</text>
<text x="882" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="495" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">4.</text>
<text x="63" y="495" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Modify with external editor</text>
<text x="882" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="512" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">5.</text>
<text x="63" y="512" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
<text x="882" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="882" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#ffffaf" textLength="891" lengthAdjust="spacingAndGlyphs">╰─────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="891" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="580" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs">Initializing...</text>
<text x="0" y="597" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="9" y="614" fill="#afafaf" textLength="225" lengthAdjust="spacingAndGlyphs">Shift+Tab to accept edits</text>
<text x="675" y="614" fill="#afafaf" textLength="216" lengthAdjust="spacingAndGlyphs">undefined undefined file</text>
<text x="9" y="631" fill="#afafaf" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text>
<text x="351" y="631" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text>
<text x="585" y="631" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text>
<text x="828" y="631" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">context</text>
<text x="9" y="648" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">/directory</text>
<text x="351" y="648" fill="#ff87af" textLength="90" lengthAdjust="spacingAndGlyphs">no sandbox</text>
<text x="585" y="648" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">gemini-pro</text>
<text x="819" y="648" fill="#afafaf" textLength="72" lengthAdjust="spacingAndGlyphs">17% used</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,44 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Full Terminal Tool Confirmation Snapshot > renders tool confirmation box in the frame of the entire terminal 1`] = `
"╭─────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Action Required │
│ │
│ ? Edit packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto… │
│─────────────────────────────────────────────────────────────────────────────────────────────────│
│ 46 const line46 = true; │
│ 47 const line47 = true; │
│ 48 const line48 = true; │
│ 49 const line49 = true; │
│ 50 const line50 = true; │
│ 51 const line51 = true; │
│ 52 const line52 = true; │
│ 53 const line53 = true; │
│ 54 const line54 = true; │
│ 55 const line55 = true; │
│ 56 const line56 = true; │
│ 57 const line57 = true; │
│ 58 const line58 = true; │
│ 59 const line59 = true; │
│ 60 const line60 = true; │
│ 61 - return kittyProtocolSupporte...; │
│ 61 + return kittyProtocolSupporte...; │
│ 62 buffer: TextBuffer; │
│ 63 onSubmit: (value: string) => void; │
│ Apply this change? │
│ │█
│ ● 1. Allow once │█
│ 2. Allow for this session │█
│ 3. Allow for this file in all future sessions │█
│ 4. Modify with external editor │█
│ 5. No, suggest changes (esc) │█
│ │█
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯█
Initializing...
────────────────────────────────────────────────────────────────────────────────────────────────────
Shift+Tab to accept edits undefined undefined file
workspace (/directory) sandbox /model context
/directory no sandbox gemini-pro 17% used
"
`;
@@ -10,6 +10,7 @@ import {
} from '../../test-utils/render.js';
import { AppHeader } from './AppHeader.js';
import { describe, it, expect, vi } from 'vitest';
import { makeFakeConfig } from '@google/gemini-cli-core';
import crypto from 'node:crypto';
vi.mock('../utils/terminalSetup.js', () => ({
@@ -240,4 +241,27 @@ describe('<AppHeader />', () => {
expect(session2.lastFrame()).not.toContain('Tips');
session2.unmount();
});
it('should render the full logo when logged out', async () => {
const mockConfig = makeFakeConfig();
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
authType: undefined,
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<AppHeader version="1.0.0" />,
{
config: mockConfig,
uiState: {
terminalWidth: 120,
},
},
);
await waitUntilReady();
// Check for block characters from the logo
expect(lastFrame()).toContain('▗█▀▀▜▙');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
+86 -51
View File
@@ -19,6 +19,9 @@ import { CliSpinner } from './CliSpinner.js';
import { isAppleTerminal } from '@google/gemini-cli-core';
import { longAsciiLogoCompactText } from './AsciiArt.js';
import { getAsciiArtWidth } from '../utils/textUtils.js';
interface AppHeaderProps {
version: string;
showDetails?: boolean;
@@ -41,6 +44,18 @@ const MAC_TERMINAL_ICON = `▝▜▄
`;
/**
* The horizontal padding (in columns) required for metadata (version, identity, etc.)
* when rendered alongside the ASCII logo.
*/
const LOGO_METADATA_PADDING = 20;
/**
* The terminal width below which we switch to a narrow/column layout to prevent
* UI elements from wrapping or overlapping.
*/
const NARROW_TERMINAL_BREAKPOINT = 60;
export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
const settings = useSettings();
const config = useConfig();
@@ -49,70 +64,90 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
const { bannerText } = useBanner(bannerData);
const { showTips } = useTips();
const authType = config.getContentGeneratorConfig()?.authType;
const loggedOut = !authType;
const showHeader = !(
settings.merged.ui.hideBanner || config.getScreenReader()
);
const ICON = isAppleTerminal() ? MAC_TERMINAL_ICON : DEFAULT_ICON;
if (!showDetails) {
return (
<Box flexDirection="column">
{showHeader && (
<Box
flexDirection="row"
marginTop={1}
marginBottom={1}
paddingLeft={2}
>
<Box flexShrink={0}>
<ThemedGradient>{ICON}</ThemedGradient>
</Box>
<Box marginLeft={2} flexDirection="column">
<Box>
<Text bold color={theme.text.primary}>
Gemini CLI
</Text>
<Text color={theme.text.secondary}> v{version}</Text>
</Box>
</Box>
let logoTextArt = '';
if (loggedOut) {
const widthOfLongLogo =
getAsciiArtWidth(longAsciiLogoCompactText) + LOGO_METADATA_PADDING;
if (terminalWidth >= widthOfLongLogo) {
logoTextArt = longAsciiLogoCompactText.trim();
}
}
// If the terminal is too narrow to fit the icon and metadata (especially long nightly versions)
// side-by-side, we switch to column mode to prevent wrapping.
const isNarrow = terminalWidth < NARROW_TERMINAL_BREAKPOINT;
const renderLogo = () => (
<Box flexDirection="row">
<Box flexShrink={0}>
<ThemedGradient>{ICON}</ThemedGradient>
</Box>
{logoTextArt && (
<Box marginLeft={3}>
<Text color={theme.text.primary}>{logoTextArt}</Text>
</Box>
)}
</Box>
);
const renderMetadata = (isBelow = false) => (
<Box marginLeft={isBelow ? 0 : 2} flexDirection="column">
{/* Line 1: Gemini CLI vVersion [Updating] */}
<Box>
<Text bold color={theme.text.primary}>
Gemini CLI
</Text>
<Text color={theme.text.secondary}> v{version}</Text>
{updateInfo && (
<Box marginLeft={2}>
<Text color={theme.text.secondary}>
<CliSpinner /> Updating
</Text>
</Box>
)}
</Box>
);
}
{showDetails && (
<>
{/* Line 2: Blank */}
<Box height={1} />
{/* Lines 3 & 4: User Identity info (Email /auth and Plan /upgrade) */}
{settings.merged.ui.showUserIdentity !== false && (
<UserIdentity config={config} />
)}
</>
)}
</Box>
);
const useColumnLayout = !!logoTextArt || isNarrow;
return (
<Box flexDirection="column">
{showHeader && (
<Box flexDirection="row" marginTop={1} marginBottom={1} paddingLeft={2}>
<Box flexShrink={0}>
<ThemedGradient>{ICON}</ThemedGradient>
</Box>
<Box marginLeft={2} flexDirection="column">
{/* Line 1: Gemini CLI vVersion [Updating] */}
<Box>
<Text bold color={theme.text.primary}>
Gemini CLI
</Text>
<Text color={theme.text.secondary}> v{version}</Text>
{updateInfo && (
<Box marginLeft={2}>
<Text color={theme.text.secondary}>
<CliSpinner /> Updating
</Text>
</Box>
)}
</Box>
{/* Line 2: Blank */}
<Box height={1} />
{/* Lines 3 & 4: User Identity info (Email /auth and Plan /upgrade) */}
{settings.merged.ui.showUserIdentity !== false && (
<UserIdentity config={config} />
)}
</Box>
<Box
flexDirection={useColumnLayout ? 'column' : 'row'}
marginTop={1}
marginBottom={1}
paddingLeft={1}
>
{renderLogo()}
{useColumnLayout ? (
<Box marginTop={1}>{renderMetadata(true)}</Box>
) : (
renderMetadata(false)
)}
</Box>
)}
+29 -8
View File
@@ -16,14 +16,14 @@ export const shortAsciiLogo = `
`;
export const longAsciiLogo = `
`;
export const tinyAsciiLogo = `
@@ -36,3 +36,24 @@ export const tinyAsciiLogo = `
`;
export const shortAsciiLogoCompactText = `
`;
export const longAsciiLogoCompactText = `
`;
export const tinyAsciiLogoCompactText = `
`;
@@ -1453,4 +1453,42 @@ describe('AskUserDialog', () => {
});
});
});
it('shows at least 3 selection options even in small terminal heights', async () => {
const questions: Question[] = [
{
question:
'A very long question that would normally take up most of the space and squeeze the list if we did not have a heuristic to prevent it. This line is just to make it longer. And another one. Imagine this is a plan.',
header: 'Test',
type: QuestionType.CHOICE,
options: [
{ label: 'Option 1', description: 'Description 1' },
{ label: 'Option 2', description: 'Description 2' },
{ label: 'Option 3', description: 'Description 3' },
{ label: 'Option 4', description: 'Description 4' },
],
multiSelect: false,
},
];
const { lastFrame, waitUntilReady } = await renderWithProviders(
<AskUserDialog
questions={questions}
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={80}
availableHeight={12} // Very small height
/>,
{ width: 80 },
);
await waitFor(async () => {
await waitUntilReady();
const frame = lastFrame();
// Should show at least 3 options
expect(frame).toContain('1. Option 1');
expect(frame).toContain('2. Option 2');
expect(frame).toContain('3. Option 3');
});
});
});
@@ -849,11 +849,19 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
? Math.max(1, availableHeight - overhead)
: undefined;
// Reserve space for at least 3 items if more selectionItems available.
const reservedListHeight = Math.min(selectionItems.length * 2, 6);
const questionHeightLimit =
listHeight && !isAlternateBuffer
? question.unconstrainedHeight
? Math.max(1, listHeight - selectionItems.length * 2)
: Math.min(15, Math.max(1, listHeight - DIALOG_PADDING))
: Math.min(
15,
Math.max(
1,
listHeight - Math.max(DIALOG_PADDING, reservedListHeight),
),
)
: undefined;
const maxItemsToShow =
+3 -1
View File
@@ -172,7 +172,9 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
}, [canShowShortcutsHint]);
const shouldReserveSpaceForShortcutsHint =
settings.merged.ui.showShortcutsHint && !hideShortcutsHintForSuggestions;
settings.merged.ui.showShortcutsHint &&
!hideShortcutsHintForSuggestions &&
!hasPendingActionRequired;
const showShortcutsHint =
shouldReserveSpaceForShortcutsHint && showShortcutsHintDebounced;
const showMinimalModeBleedThrough =
@@ -10,7 +10,7 @@ import * as SessionContext from '../contexts/SessionContext.js';
import { type SessionStatsState } from '../contexts/SessionContext.js';
import { Banner } from './Banner.js';
import { Footer } from './Footer.js';
import { Header } from './Header.js';
import { AppHeader } from './AppHeader.js';
import { ModelDialog } from './ModelDialog.js';
import { StatsDisplay } from './StatsDisplay.js';
@@ -71,9 +71,9 @@ useSessionStatsMock.mockReturnValue({
});
describe('Gradient Crash Regression Tests', () => {
it('<Header /> should not crash when theme.ui.gradient is empty', async () => {
it('<AppHeader /> should not crash when theme.ui.gradient is empty', async () => {
const { lastFrame, unmount } = await renderWithProviders(
<Header version="1.0.0" nightly={false} />,
<AppHeader version="1.0.0" />,
{
width: 120,
},
@@ -6,13 +6,16 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { act } from 'react';
import { Box } from 'ink';
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
import { StreamingState } from '../types.js';
import { renderWithProviders } from '../../test-utils/render.js';
import { createMockSettings } from '../../test-utils/settings.js';
import { waitFor } from '../../test-utils/async.js';
import { type Config, CoreToolCallStatus } from '@google/gemini-cli-core';
import {
type Config,
CoreToolCallStatus,
type SerializableConfirmationDetails,
} from '@google/gemini-cli-core';
import type { ConfirmingToolState } from '../hooks/useConfirmingTool.js';
import { theme } from '../semantic-colors.js';
@@ -133,59 +136,6 @@ describe('ToolConfirmationQueue', () => {
unmount();
});
it('renders expansion hint when content is long and constrained', async () => {
const longDiff = '@@ -1,1 +1,50 @@\n' + '+line\n'.repeat(50);
const confirmingTool = {
tool: {
callId: 'call-1',
name: 'replace',
description: 'edit file',
status: CoreToolCallStatus.AwaitingApproval,
confirmationDetails: {
type: 'edit' as const,
title: 'Confirm edit',
fileName: 'test.ts',
filePath: '/test.ts',
fileDiff: longDiff,
originalContent: 'old',
newContent: 'new',
},
},
index: 1,
total: 1,
};
const { lastFrame, unmount } = await renderWithProviders(
<Box flexDirection="column" height={30}>
<ToolConfirmationQueue
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
/>
</Box>,
{
config: {
// eslint-disable-next-line @typescript-eslint/no-misused-spread
...mockConfig,
getUseAlternateBuffer: () => true,
} as unknown as Config,
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
uiState: {
terminalWidth: 80,
terminalHeight: 20,
constrainHeight: true,
streamingState: StreamingState.WaitingForConfirmation,
},
},
);
await waitFor(() =>
expect(lastFrame()?.toLowerCase()).toContain(
'press ctrl+o to show more lines',
),
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('calculates availableContentHeight based on availableTerminalHeight from UI state', async () => {
const longDiff = '@@ -1,1 +1,50 @@\n' + '+line\n'.repeat(50);
const confirmingTool = {
@@ -414,4 +364,155 @@ describe('ToolConfirmationQueue', () => {
expect(stickyHeaderProps.borderColor).toBe(theme.status.success);
unmount();
});
describe('height allocation and layout', () => {
it('should render the full queue wrapper with borders and content for large edit diffs', async () => {
let largeDiff = '--- a/file.ts\n+++ b/file.ts\n@@ -1,10 +1,15 @@\n';
for (let i = 1; i <= 20; i++) {
largeDiff += `-const oldLine${i} = true;\n`;
largeDiff += `+const newLine${i} = true;\n`;
}
const confirmationDetails: SerializableConfirmationDetails = {
type: 'edit',
title: 'Confirm Edit',
fileName: 'file.ts',
filePath: '/file.ts',
fileDiff: largeDiff,
originalContent: 'old',
newContent: 'new',
isModifying: false,
};
const confirmingTool = {
tool: {
callId: 'test-call-id',
name: 'replace',
status: CoreToolCallStatus.AwaitingApproval,
description: 'Replaces content in a file',
confirmationDetails,
},
index: 1,
total: 1,
};
const { waitUntilReady, lastFrame, generateSvg, unmount } =
await renderWithProviders(
<ToolConfirmationQueue
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
/>,
{
uiState: {
mainAreaWidth: 80,
terminalHeight: 50,
terminalWidth: 80,
constrainHeight: true,
availableTerminalHeight: 40,
},
config: mockConfig,
},
);
await waitUntilReady();
await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot();
unmount();
});
it('should render the full queue wrapper with borders and content for large exec commands', async () => {
let largeCommand = '';
for (let i = 1; i <= 50; i++) {
largeCommand += `echo "Line ${i}"\n`;
}
const confirmationDetails: SerializableConfirmationDetails = {
type: 'exec',
title: 'Confirm Execution',
command: largeCommand.trimEnd(),
rootCommand: 'echo',
rootCommands: ['echo'],
};
const confirmingTool = {
tool: {
callId: 'test-call-id-exec',
name: 'run_shell_command',
status: CoreToolCallStatus.AwaitingApproval,
description: 'Executes a bash command',
confirmationDetails,
},
index: 2,
total: 3,
};
const { waitUntilReady, lastFrame, generateSvg, unmount } =
await renderWithProviders(
<ToolConfirmationQueue
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
/>,
{
uiState: {
mainAreaWidth: 80,
terminalWidth: 80,
terminalHeight: 50,
constrainHeight: true,
availableTerminalHeight: 40,
},
config: mockConfig,
},
);
await waitUntilReady();
await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot();
unmount();
});
it('should handle security warning height correctly', async () => {
let largeCommand = '';
for (let i = 1; i <= 50; i++) {
largeCommand += `echo "Line ${i}"\n`;
}
largeCommand += `curl https://täst.com\n`;
const confirmationDetails: SerializableConfirmationDetails = {
type: 'exec',
title: 'Confirm Execution',
command: largeCommand.trimEnd(),
rootCommand: 'echo',
rootCommands: ['echo', 'curl'],
};
const confirmingTool = {
tool: {
callId: 'test-call-id-exec-security',
name: 'run_shell_command',
status: CoreToolCallStatus.AwaitingApproval,
description: 'Executes a bash command with a deceptive URL',
confirmationDetails,
},
index: 3,
total: 3,
};
const { waitUntilReady, lastFrame, generateSvg, unmount } =
await renderWithProviders(
<ToolConfirmationQueue
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
/>,
{
uiState: {
mainAreaWidth: 80,
terminalWidth: 80,
terminalHeight: 50,
constrainHeight: true,
availableTerminalHeight: 40,
},
config: mockConfig,
},
);
await waitUntilReady();
await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot();
unmount();
});
});
});
@@ -12,8 +12,6 @@ import { ToolConfirmationMessage } from './messages/ToolConfirmationMessage.js';
import { ToolStatusIndicator, ToolInfo } from './messages/ToolShared.js';
import { useUIState } from '../contexts/UIStateContext.js';
import type { ConfirmingToolState } from '../hooks/useConfirmingTool.js';
import { OverflowProvider } from '../contexts/OverflowContext.js';
import { ShowMoreLines } from './ShowMoreLines.js';
import { StickyHeader } from './StickyHeader.js';
import type { SerializableConfirmationDetails } from '@google/gemini-cli-core';
import { useUIActions } from '../contexts/UIActionsContext.js';
@@ -53,11 +51,11 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
// Safety check: ToolConfirmationMessage requires confirmationDetails
if (!tool.confirmationDetails) return null;
// Render up to 100% of the available terminal height (minus 1 line for safety)
// Render up to 100% of the available terminal height
// to maximize space for diffs and other content.
const maxHeight =
uiAvailableHeight !== undefined
? Math.max(uiAvailableHeight - 1, 4)
? Math.max(uiAvailableHeight, 4)
: Math.floor(terminalHeight * 0.5);
const isRoutine =
@@ -76,84 +74,81 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
: undefined;
const content = (
<>
<Box flexDirection="column" width={mainAreaWidth} flexShrink={0}>
<StickyHeader
width={mainAreaWidth}
isFirst={true}
borderColor={borderColor}
borderDimColor={false}
>
<Box flexDirection="column" width={mainAreaWidth - 4}>
{/* Header */}
<Box
marginBottom={hideToolIdentity ? 0 : 1}
justifyContent="space-between"
>
<Text color={borderColor} bold>
{getConfirmationHeader(tool.confirmationDetails)}
<Box flexDirection="column" width={mainAreaWidth} flexShrink={0}>
<StickyHeader
width={mainAreaWidth}
isFirst={true}
borderColor={borderColor}
borderDimColor={false}
>
<Box flexDirection="column" width={mainAreaWidth - 4}>
{/* Header */}
<Box
marginBottom={hideToolIdentity ? 0 : 1}
justifyContent="space-between"
>
<Text color={borderColor} bold>
{getConfirmationHeader(tool.confirmationDetails)}
</Text>
{total > 1 && (
<Text color={theme.text.secondary}>
{index} of {total}
</Text>
{total > 1 && (
<Text color={theme.text.secondary}>
{index} of {total}
</Text>
)}
</Box>
{!hideToolIdentity && (
<Box>
<ToolStatusIndicator status={tool.status} name={tool.name} />
<ToolInfo
name={tool.name}
status={tool.status}
description={tool.description}
emphasis="high"
/>
</Box>
)}
</Box>
</StickyHeader>
<Box
width={mainAreaWidth}
borderStyle="round"
borderColor={borderColor}
borderTop={false}
borderBottom={false}
borderLeft={true}
borderRight={true}
paddingX={1}
flexDirection="column"
>
{/* Interactive Area */}
{/*
Note: We force isFocused={true} because if this component is rendered,
it effectively acts as a modal over the shell/composer.
*/}
<ToolConfirmationMessage
callId={tool.callId}
confirmationDetails={tool.confirmationDetails}
config={config}
getPreferredEditor={getPreferredEditor}
terminalWidth={mainAreaWidth - 4} // Adjust for parent border/padding
availableTerminalHeight={availableContentHeight}
isFocused={true}
/>
{!hideToolIdentity && (
<Box>
<ToolStatusIndicator status={tool.status} name={tool.name} />
<ToolInfo
name={tool.name}
status={tool.status}
description={tool.description}
emphasis="high"
/>
</Box>
)}
</Box>
<Box
height={1}
width={mainAreaWidth}
borderLeft={true}
borderRight={true}
borderTop={false}
borderBottom={true}
borderColor={borderColor}
borderStyle="round"
</StickyHeader>
<Box
width={mainAreaWidth}
borderStyle="round"
borderColor={borderColor}
borderTop={false}
borderBottom={false}
borderLeft={true}
borderRight={true}
paddingX={1}
flexDirection="column"
>
{/* Interactive Area */}
{/*
Note: We force isFocused={true} because if this component is rendered,
it effectively acts as a modal over the shell/composer.
*/}
<ToolConfirmationMessage
callId={tool.callId}
confirmationDetails={tool.confirmationDetails}
config={config}
getPreferredEditor={getPreferredEditor}
terminalWidth={mainAreaWidth - 4} // Adjust for parent border/padding
availableTerminalHeight={availableContentHeight}
isFocused={true}
/>
</Box>
<ShowMoreLines constrainHeight={constrainHeight} />
</>
<Box
height={1}
width={mainAreaWidth}
borderLeft={true}
borderRight={true}
borderTop={false}
borderBottom={true}
borderColor={borderColor}
borderStyle="round"
/>
</Box>
);
return <OverflowProvider>{content}</OverflowProvider>;
return content;
};
@@ -2,10 +2,13 @@
exports[`AlternateBufferQuittingDisplay > renders with a tool awaiting confirmation > with_confirming_tool 1`] = `
"
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v0.10.0
Tips for getting started:
@@ -22,10 +25,13 @@ Action Required (was prompted):
exports[`AlternateBufferQuittingDisplay > renders with active and pending tool messages > with_history_and_pending 1`] = `
"
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v0.10.0
Tips for getting started:
@@ -50,10 +56,13 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with empty history and no pending items > empty 1`] = `
"
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v0.10.0
Tips for getting started:
@@ -66,10 +75,13 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with history but no pending items > with_history_no_pending 1`] = `
"
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v0.10.0
Tips for getting started:
@@ -90,10 +102,13 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_pending_no_history 1`] = `
"
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v0.10.0
Tips for getting started:
@@ -110,10 +125,13 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages > with_user_gemini_messages 1`] = `
"
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v0.10.0
Tips for getting started:
@@ -2,10 +2,13 @@
exports[`<AppHeader /> > should not render the banner when no flags are set 1`] = `
"
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.0.0
Tips for getting started:
@@ -18,10 +21,13 @@ Tips for getting started:
exports[`<AppHeader /> > should not render the default banner if shown count is 5 or more 1`] = `
"
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.0.0
Tips for getting started:
@@ -34,10 +40,13 @@ Tips for getting started:
exports[`<AppHeader /> > should render the banner with default text 1`] = `
"
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.0.0
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ This is the default banner │
@@ -53,10 +62,13 @@ Tips for getting started:
exports[`<AppHeader /> > should render the banner with warning text 1`] = `
"
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.0.0
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ There are capacity issues │
@@ -69,3 +81,14 @@ Tips for getting started:
4. Be specific for the best results
"
`;
exports[`<AppHeader /> > should render the full logo when logged out 1`] = `
"
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.0.0
"
`;
@@ -1,30 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="224" viewBox="0 0 920 224">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="275" viewBox="0 0 920 275">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="224" fill="#000000" />
<rect width="920" height="275" fill="#000000" />
<g transform="translate(10, 10)">
<text x="18" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="180" y="19" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.0.0</text>
<text x="36" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="121" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="138" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="138" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="138" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="155" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="155" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="155" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="172" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="189" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
<text x="9" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="19" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛</text>
<text x="27" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="36" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">█▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌</text>
<text x="18" y="53" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="53" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌</text>
<text x="9" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="70" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀</text>
<text x="9" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="99" y="104" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.0.0</text>
<text x="0" y="172" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="189" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="189" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="189" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="206" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="206" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="206" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="223" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="240" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -1,31 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="224" viewBox="0 0 920 224">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="275" viewBox="0 0 920 275">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="224" fill="#000000" />
<rect width="920" height="275" fill="#000000" />
<g transform="translate(10, 10)">
<text x="18" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="81" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="171" y="19" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.0.0</text>
<text x="36" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="53" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="70" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="121" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="138" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="138" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="138" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="155" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="155" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="155" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="172" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="189" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
<text x="9" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="81" y="19" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛</text>
<text x="27" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="81" y="36" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">█▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌</text>
<text x="27" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="53" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="81" y="53" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌</text>
<text x="9" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="70" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="81" y="70" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀</text>
<text x="9" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="99" y="104" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.0.0</text>
<text x="0" y="172" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="189" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="189" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="189" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="206" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="206" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="206" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="223" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="240" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -2,10 +2,13 @@
exports[`AppHeader Icon Rendering > renders the default icon in standard terminals 1`] = `
"
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.0.0
Tips for getting started:
@@ -17,10 +20,13 @@ Tips for getting started:
exports[`AppHeader Icon Rendering > renders the symmetric icon in Apple Terminal 1`] = `
"
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▗▟▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▗▟▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.0.0
Tips for getting started:
@@ -52,6 +52,8 @@ exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scrol
Description 1
2. Option 2
Description 2
3. Option 3
Description 3
Enter to select · ↑/↓ to navigate · Esc to cancel
@@ -14,24 +14,12 @@ Spinner Initializing...
exports[`ConfigInitDisplay > truncates list of waiting servers if too many 1`] = `
"
Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more
"
`;
exports[`ConfigInitDisplay > truncates list of waiting servers if too many 2`] = `
"
Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more
Spinner Initializing...
"
`;
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = `
"
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
"
`;
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 2`] = `
"
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
Spinner Initializing...
"
`;
@@ -21,22 +21,22 @@
<text x="9" y="121" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="121" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Initial analysis</text>
<text x="9" y="138" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="138" fill="#afafaf" textLength="846" lengthAdjust="spacingAndGlyphs" font-style="italic">This is a multiple line paragraph for the first thinking message of how the model analyzes the</text>
<text x="27" y="138" fill="#afafaf" textLength="675" lengthAdjust="spacingAndGlyphs" font-style="italic">This is a multiple line paragraph for the first thinking message of how the</text>
<text x="9" y="155" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="155" fill="#afafaf" textLength="72" lengthAdjust="spacingAndGlyphs" font-style="italic">problem.</text>
<text x="27" y="155" fill="#afafaf" textLength="243" lengthAdjust="spacingAndGlyphs" font-style="italic">model analyzes the problem.</text>
<text x="9" y="172" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="189" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="189" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Planning execution</text>
<text x="9" y="206" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="206" fill="#afafaf" textLength="828" lengthAdjust="spacingAndGlyphs" font-style="italic">This a second multiple line paragraph for the second thinking message explaining the plan in</text>
<text x="27" y="206" fill="#afafaf" textLength="621" lengthAdjust="spacingAndGlyphs" font-style="italic">This a second multiple line paragraph for the second thinking message</text>
<text x="9" y="223" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="223" fill="#afafaf" textLength="468" lengthAdjust="spacingAndGlyphs" font-style="italic">detail so that it wraps around the terminal display.</text>
<text x="27" y="223" fill="#afafaf" textLength="675" lengthAdjust="spacingAndGlyphs" font-style="italic">explaining the plan in detail so that it wraps around the terminal display.</text>
<text x="9" y="240" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="257" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="257" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Refining approach</text>
<text x="9" y="274" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="274" fill="#afafaf" textLength="792" lengthAdjust="spacingAndGlyphs" font-style="italic">And finally a third multiple line paragraph for the third thinking message to refine the</text>
<text x="27" y="274" fill="#afafaf" textLength="693" lengthAdjust="spacingAndGlyphs" font-style="italic">And finally a third multiple line paragraph for the third thinking message to</text>
<text x="9" y="291" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="291" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs" font-style="italic">solution.</text>
<text x="27" y="291" fill="#afafaf" textLength="180" lengthAdjust="spacingAndGlyphs" font-style="italic">refine the solution.</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -96,15 +96,15 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unc
exports[`MainContent > renders a split tool group without a gap between static and pending areas 1`] = `
"AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────
│ ✓ test-tool A tool for testing
│ Part 1
│ ✓ test-tool A tool for testing
│ Part 2
╰──────────────────────────────────────────────────────────────────────────────────────────────
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
│ │
│ Part 1 │
│ │
│ ✓ test-tool A tool for testing │
│ │
│ Part 2 │
╰──────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -163,16 +163,16 @@ AppHeader(full)
Thinking...
│ Initial analysis
│ This is a multiple line paragraph for the first thinking message of how the model analyzes the
│ problem.
│ This is a multiple line paragraph for the first thinking message of how the
model analyzes the problem.
│ Planning execution
│ This a second multiple line paragraph for the second thinking message explaining the plan in
│ detail so that it wraps around the terminal display.
│ This a second multiple line paragraph for the second thinking message
explaining the plan in detail so that it wraps around the terminal display.
│ Refining approach
│ And finally a third multiple line paragraph for the third thinking message to refine the
│ solution.
│ And finally a third multiple line paragraph for the third thinking message to
refine the solution.
"
`;
@@ -185,14 +185,14 @@ AppHeader(full)
Thinking...
│ Initial analysis
│ This is a multiple line paragraph for the first thinking message of how the model analyzes the
│ problem.
│ This is a multiple line paragraph for the first thinking message of how the
model analyzes the problem.
│ Planning execution
│ This a second multiple line paragraph for the second thinking message explaining the plan in
│ detail so that it wraps around the terminal display.
│ This a second multiple line paragraph for the second thinking message
explaining the plan in detail so that it wraps around the terminal display.
│ Refining approach
│ And finally a third multiple line paragraph for the third thinking message to refine the
│ solution."
│ And finally a third multiple line paragraph for the third thinking message to
refine the solution."
`;
@@ -0,0 +1,130 @@
<svg xmlns="http://www.w3.org/2000/svg" width="740" height="598" viewBox="0 0 740 598">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="740" height="598" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#ffffaf" textLength="720" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#ffffaf" textLength="135" lengthAdjust="spacingAndGlyphs" font-weight="bold">Action Required</text>
<text x="648" y="19" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">3 of 3</text>
<text x="711" y="19" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="36" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="53" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs">?</text>
<text x="45" y="53" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">run_shell_command</text>
<text x="207" y="53" fill="#afafaf" textLength="396" lengthAdjust="spacingAndGlyphs">Executes a bash command with a deceptive URL</text>
<text x="711" y="53" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="87" fill="#afafaf" textLength="225" lengthAdjust="spacingAndGlyphs">... 6 hidden (Ctrl+O) ...</text>
<text x="711" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="104" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="104" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 37&quot;</text>
<text x="711" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="121" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="121" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 38&quot;</text>
<text x="711" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="138" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="138" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 39&quot;</text>
<text x="711" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="155" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="155" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 40&quot;</text>
<text x="711" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="172" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="172" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 41&quot;</text>
<text x="711" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="189" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="189" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 42&quot;</text>
<text x="711" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="206" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="206" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 43&quot;</text>
<text x="711" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="223" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="223" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 44&quot;</text>
<text x="711" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="240" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="240" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 45&quot;</text>
<text x="711" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="257" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="257" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 46&quot;</text>
<text x="711" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="274" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="274" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 47&quot;</text>
<text x="711" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="291" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="291" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 48&quot;</text>
<text x="711" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="308" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="308" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 49&quot;</text>
<text x="711" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="325" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="325" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 50&quot;</text>
<text x="711" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="342" fill="#e5e5e5" textLength="189" lengthAdjust="spacingAndGlyphs">curl https://täst.com</text>
<text x="711" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="376" fill="#ffffaf" textLength="18" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#ffffaf" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Warning:</text>
<text x="117" y="376" fill="#ffffaf" textLength="243" lengthAdjust="spacingAndGlyphs"> Deceptive URL(s) detected:</text>
<text x="711" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="72" y="410" fill="#ffffaf" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">Original:</text>
<text x="162" y="410" fill="#87afff" textLength="153" lengthAdjust="spacingAndGlyphs">https://täst.com/</text>
<text x="711" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="72" y="427" fill="#ffffaf" textLength="207" lengthAdjust="spacingAndGlyphs" font-weight="bold">Actual Host (Punycode):</text>
<text x="288" y="427" fill="#87afff" textLength="216" lengthAdjust="spacingAndGlyphs">https://xn--tst-qla.com/</text>
<text x="711" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="461" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Allow execution of: &apos;echo&apos;?</text>
<text x="711" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="493" width="9" height="17" fill="#001a00" />
<text x="18" y="495" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="493" width="9" height="17" fill="#001a00" />
<rect x="36" y="493" width="18" height="17" fill="#001a00" />
<text x="36" y="495" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
<rect x="54" y="493" width="9" height="17" fill="#001a00" />
<rect x="63" y="493" width="90" height="17" fill="#001a00" />
<text x="63" y="495" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
<rect x="153" y="493" width="135" height="17" fill="#001a00" />
<text x="711" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="512" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">2.</text>
<text x="63" y="512" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Allow for this session</text>
<text x="711" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="529" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
<text x="63" y="529" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
<text x="711" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="546" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#ffffaf" textLength="720" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,458 @@
<svg xmlns="http://www.w3.org/2000/svg" width="740" height="683" viewBox="0 0 740 683">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="740" height="683" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#ffffaf" textLength="720" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#ffffaf" textLength="135" lengthAdjust="spacingAndGlyphs" font-weight="bold">Action Required</text>
<text x="711" y="19" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="36" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="53" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">?</text>
<text x="45" y="53" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs" font-weight="bold">replace</text>
<text x="117" y="53" fill="#afafaf" textLength="234" lengthAdjust="spacingAndGlyphs">Replaces content in a file</text>
<text x="711" y="53" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="87" fill="#afafaf" textLength="234" lengthAdjust="spacingAndGlyphs">... 15 hidden (Ctrl+O) ...</text>
<text x="711" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="102" width="9" height="17" fill="#005f00" />
<rect x="27" y="102" width="9" height="17" fill="#005f00" />
<text x="27" y="104" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">8</text>
<rect x="36" y="102" width="9" height="17" fill="#005f00" />
<rect x="45" y="102" width="9" height="17" fill="#005f00" />
<text x="45" y="104" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="102" width="9" height="17" fill="#005f00" />
<rect x="63" y="102" width="45" height="17" fill="#005f00" />
<text x="63" y="104" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="102" width="108" height="17" fill="#005f00" />
<text x="108" y="104" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> newLine8 = </text>
<rect x="216" y="102" width="36" height="17" fill="#005f00" />
<text x="216" y="104" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="252" y="102" width="9" height="17" fill="#005f00" />
<text x="252" y="104" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="119" width="9" height="17" fill="#5f0000" />
<rect x="27" y="119" width="9" height="17" fill="#5f0000" />
<text x="27" y="121" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">9</text>
<rect x="36" y="119" width="9" height="17" fill="#5f0000" />
<rect x="45" y="119" width="9" height="17" fill="#5f0000" />
<text x="45" y="121" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="119" width="9" height="17" fill="#5f0000" />
<rect x="63" y="119" width="45" height="17" fill="#5f0000" />
<text x="63" y="121" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="119" width="108" height="17" fill="#5f0000" />
<text x="108" y="121" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> oldLine9 = </text>
<rect x="216" y="119" width="36" height="17" fill="#5f0000" />
<text x="216" y="121" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="252" y="119" width="9" height="17" fill="#5f0000" />
<text x="252" y="121" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="136" width="9" height="17" fill="#005f00" />
<rect x="27" y="136" width="9" height="17" fill="#005f00" />
<text x="27" y="138" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">9</text>
<rect x="36" y="136" width="9" height="17" fill="#005f00" />
<rect x="45" y="136" width="9" height="17" fill="#005f00" />
<text x="45" y="138" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="136" width="9" height="17" fill="#005f00" />
<rect x="63" y="136" width="45" height="17" fill="#005f00" />
<text x="63" y="138" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="136" width="108" height="17" fill="#005f00" />
<text x="108" y="138" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> newLine9 = </text>
<rect x="216" y="136" width="36" height="17" fill="#005f00" />
<text x="216" y="138" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="252" y="136" width="9" height="17" fill="#005f00" />
<text x="252" y="138" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="153" width="18" height="17" fill="#5f0000" />
<text x="18" y="155" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<rect x="36" y="153" width="9" height="17" fill="#5f0000" />
<rect x="45" y="153" width="9" height="17" fill="#5f0000" />
<text x="45" y="155" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="153" width="9" height="17" fill="#5f0000" />
<rect x="63" y="153" width="45" height="17" fill="#5f0000" />
<text x="63" y="155" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="153" width="117" height="17" fill="#5f0000" />
<text x="108" y="155" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine10 = </text>
<rect x="225" y="153" width="36" height="17" fill="#5f0000" />
<text x="225" y="155" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="153" width="9" height="17" fill="#5f0000" />
<text x="261" y="155" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="170" width="18" height="17" fill="#005f00" />
<text x="18" y="172" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<rect x="36" y="170" width="9" height="17" fill="#005f00" />
<rect x="45" y="170" width="9" height="17" fill="#005f00" />
<text x="45" y="172" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="170" width="9" height="17" fill="#005f00" />
<rect x="63" y="170" width="45" height="17" fill="#005f00" />
<text x="63" y="172" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="170" width="117" height="17" fill="#005f00" />
<text x="108" y="172" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine10 = </text>
<rect x="225" y="170" width="36" height="17" fill="#005f00" />
<text x="225" y="172" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="170" width="9" height="17" fill="#005f00" />
<text x="261" y="172" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="187" width="18" height="17" fill="#5f0000" />
<text x="18" y="189" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">11</text>
<rect x="36" y="187" width="9" height="17" fill="#5f0000" />
<rect x="45" y="187" width="9" height="17" fill="#5f0000" />
<text x="45" y="189" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="187" width="9" height="17" fill="#5f0000" />
<rect x="63" y="187" width="45" height="17" fill="#5f0000" />
<text x="63" y="189" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="187" width="117" height="17" fill="#5f0000" />
<text x="108" y="189" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine11 = </text>
<rect x="225" y="187" width="36" height="17" fill="#5f0000" />
<text x="225" y="189" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="187" width="9" height="17" fill="#5f0000" />
<text x="261" y="189" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="204" width="18" height="17" fill="#005f00" />
<text x="18" y="206" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">11</text>
<rect x="36" y="204" width="9" height="17" fill="#005f00" />
<rect x="45" y="204" width="9" height="17" fill="#005f00" />
<text x="45" y="206" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="204" width="9" height="17" fill="#005f00" />
<rect x="63" y="204" width="45" height="17" fill="#005f00" />
<text x="63" y="206" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="204" width="117" height="17" fill="#005f00" />
<text x="108" y="206" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine11 = </text>
<rect x="225" y="204" width="36" height="17" fill="#005f00" />
<text x="225" y="206" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="204" width="9" height="17" fill="#005f00" />
<text x="261" y="206" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="221" width="18" height="17" fill="#5f0000" />
<text x="18" y="223" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">12</text>
<rect x="36" y="221" width="9" height="17" fill="#5f0000" />
<rect x="45" y="221" width="9" height="17" fill="#5f0000" />
<text x="45" y="223" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="221" width="9" height="17" fill="#5f0000" />
<rect x="63" y="221" width="45" height="17" fill="#5f0000" />
<text x="63" y="223" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="221" width="117" height="17" fill="#5f0000" />
<text x="108" y="223" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine12 = </text>
<rect x="225" y="221" width="36" height="17" fill="#5f0000" />
<text x="225" y="223" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="221" width="9" height="17" fill="#5f0000" />
<text x="261" y="223" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="238" width="18" height="17" fill="#005f00" />
<text x="18" y="240" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">12</text>
<rect x="36" y="238" width="9" height="17" fill="#005f00" />
<rect x="45" y="238" width="9" height="17" fill="#005f00" />
<text x="45" y="240" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="238" width="9" height="17" fill="#005f00" />
<rect x="63" y="238" width="45" height="17" fill="#005f00" />
<text x="63" y="240" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="238" width="117" height="17" fill="#005f00" />
<text x="108" y="240" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine12 = </text>
<rect x="225" y="238" width="36" height="17" fill="#005f00" />
<text x="225" y="240" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="238" width="9" height="17" fill="#005f00" />
<text x="261" y="240" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="255" width="18" height="17" fill="#5f0000" />
<text x="18" y="257" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">13</text>
<rect x="36" y="255" width="9" height="17" fill="#5f0000" />
<rect x="45" y="255" width="9" height="17" fill="#5f0000" />
<text x="45" y="257" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="255" width="9" height="17" fill="#5f0000" />
<rect x="63" y="255" width="45" height="17" fill="#5f0000" />
<text x="63" y="257" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="255" width="117" height="17" fill="#5f0000" />
<text x="108" y="257" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine13 = </text>
<rect x="225" y="255" width="36" height="17" fill="#5f0000" />
<text x="225" y="257" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="255" width="9" height="17" fill="#5f0000" />
<text x="261" y="257" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="272" width="18" height="17" fill="#005f00" />
<text x="18" y="274" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">13</text>
<rect x="36" y="272" width="9" height="17" fill="#005f00" />
<rect x="45" y="272" width="9" height="17" fill="#005f00" />
<text x="45" y="274" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="272" width="9" height="17" fill="#005f00" />
<rect x="63" y="272" width="45" height="17" fill="#005f00" />
<text x="63" y="274" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="272" width="117" height="17" fill="#005f00" />
<text x="108" y="274" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine13 = </text>
<rect x="225" y="272" width="36" height="17" fill="#005f00" />
<text x="225" y="274" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="272" width="9" height="17" fill="#005f00" />
<text x="261" y="274" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="289" width="18" height="17" fill="#5f0000" />
<text x="18" y="291" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">14</text>
<rect x="36" y="289" width="9" height="17" fill="#5f0000" />
<rect x="45" y="289" width="9" height="17" fill="#5f0000" />
<text x="45" y="291" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="289" width="9" height="17" fill="#5f0000" />
<rect x="63" y="289" width="45" height="17" fill="#5f0000" />
<text x="63" y="291" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="289" width="117" height="17" fill="#5f0000" />
<text x="108" y="291" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine14 = </text>
<rect x="225" y="289" width="36" height="17" fill="#5f0000" />
<text x="225" y="291" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="289" width="9" height="17" fill="#5f0000" />
<text x="261" y="291" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="306" width="18" height="17" fill="#005f00" />
<text x="18" y="308" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">14</text>
<rect x="36" y="306" width="9" height="17" fill="#005f00" />
<rect x="45" y="306" width="9" height="17" fill="#005f00" />
<text x="45" y="308" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="306" width="9" height="17" fill="#005f00" />
<rect x="63" y="306" width="45" height="17" fill="#005f00" />
<text x="63" y="308" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="306" width="117" height="17" fill="#005f00" />
<text x="108" y="308" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine14 = </text>
<rect x="225" y="306" width="36" height="17" fill="#005f00" />
<text x="225" y="308" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="306" width="9" height="17" fill="#005f00" />
<text x="261" y="308" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="323" width="18" height="17" fill="#5f0000" />
<text x="18" y="325" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">15</text>
<rect x="36" y="323" width="9" height="17" fill="#5f0000" />
<rect x="45" y="323" width="9" height="17" fill="#5f0000" />
<text x="45" y="325" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="323" width="9" height="17" fill="#5f0000" />
<rect x="63" y="323" width="45" height="17" fill="#5f0000" />
<text x="63" y="325" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="323" width="117" height="17" fill="#5f0000" />
<text x="108" y="325" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine15 = </text>
<rect x="225" y="323" width="36" height="17" fill="#5f0000" />
<text x="225" y="325" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="323" width="9" height="17" fill="#5f0000" />
<text x="261" y="325" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="340" width="18" height="17" fill="#005f00" />
<text x="18" y="342" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">15</text>
<rect x="36" y="340" width="9" height="17" fill="#005f00" />
<rect x="45" y="340" width="9" height="17" fill="#005f00" />
<text x="45" y="342" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="340" width="9" height="17" fill="#005f00" />
<rect x="63" y="340" width="45" height="17" fill="#005f00" />
<text x="63" y="342" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="340" width="117" height="17" fill="#005f00" />
<text x="108" y="342" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine15 = </text>
<rect x="225" y="340" width="36" height="17" fill="#005f00" />
<text x="225" y="342" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="340" width="9" height="17" fill="#005f00" />
<text x="261" y="342" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="357" width="18" height="17" fill="#5f0000" />
<text x="18" y="359" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">16</text>
<rect x="36" y="357" width="9" height="17" fill="#5f0000" />
<rect x="45" y="357" width="9" height="17" fill="#5f0000" />
<text x="45" y="359" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="357" width="9" height="17" fill="#5f0000" />
<rect x="63" y="357" width="45" height="17" fill="#5f0000" />
<text x="63" y="359" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="357" width="117" height="17" fill="#5f0000" />
<text x="108" y="359" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine16 = </text>
<rect x="225" y="357" width="36" height="17" fill="#5f0000" />
<text x="225" y="359" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="357" width="9" height="17" fill="#5f0000" />
<text x="261" y="359" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="374" width="18" height="17" fill="#005f00" />
<text x="18" y="376" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">16</text>
<rect x="36" y="374" width="9" height="17" fill="#005f00" />
<rect x="45" y="374" width="9" height="17" fill="#005f00" />
<text x="45" y="376" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="374" width="9" height="17" fill="#005f00" />
<rect x="63" y="374" width="45" height="17" fill="#005f00" />
<text x="63" y="376" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="374" width="117" height="17" fill="#005f00" />
<text x="108" y="376" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine16 = </text>
<rect x="225" y="374" width="36" height="17" fill="#005f00" />
<text x="225" y="376" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="374" width="9" height="17" fill="#005f00" />
<text x="261" y="376" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="391" width="18" height="17" fill="#5f0000" />
<text x="18" y="393" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">17</text>
<rect x="36" y="391" width="9" height="17" fill="#5f0000" />
<rect x="45" y="391" width="9" height="17" fill="#5f0000" />
<text x="45" y="393" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="391" width="9" height="17" fill="#5f0000" />
<rect x="63" y="391" width="45" height="17" fill="#5f0000" />
<text x="63" y="393" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="391" width="117" height="17" fill="#5f0000" />
<text x="108" y="393" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine17 = </text>
<rect x="225" y="391" width="36" height="17" fill="#5f0000" />
<text x="225" y="393" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="391" width="9" height="17" fill="#5f0000" />
<text x="261" y="393" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="408" width="18" height="17" fill="#005f00" />
<text x="18" y="410" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">17</text>
<rect x="36" y="408" width="9" height="17" fill="#005f00" />
<rect x="45" y="408" width="9" height="17" fill="#005f00" />
<text x="45" y="410" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="408" width="9" height="17" fill="#005f00" />
<rect x="63" y="408" width="45" height="17" fill="#005f00" />
<text x="63" y="410" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="408" width="117" height="17" fill="#005f00" />
<text x="108" y="410" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine17 = </text>
<rect x="225" y="408" width="36" height="17" fill="#005f00" />
<text x="225" y="410" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="408" width="9" height="17" fill="#005f00" />
<text x="261" y="410" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="425" width="18" height="17" fill="#5f0000" />
<text x="18" y="427" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">18</text>
<rect x="36" y="425" width="9" height="17" fill="#5f0000" />
<rect x="45" y="425" width="9" height="17" fill="#5f0000" />
<text x="45" y="427" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="425" width="9" height="17" fill="#5f0000" />
<rect x="63" y="425" width="45" height="17" fill="#5f0000" />
<text x="63" y="427" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="425" width="117" height="17" fill="#5f0000" />
<text x="108" y="427" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine18 = </text>
<rect x="225" y="425" width="36" height="17" fill="#5f0000" />
<text x="225" y="427" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="425" width="9" height="17" fill="#5f0000" />
<text x="261" y="427" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="442" width="18" height="17" fill="#005f00" />
<text x="18" y="444" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">18</text>
<rect x="36" y="442" width="9" height="17" fill="#005f00" />
<rect x="45" y="442" width="9" height="17" fill="#005f00" />
<text x="45" y="444" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="442" width="9" height="17" fill="#005f00" />
<rect x="63" y="442" width="45" height="17" fill="#005f00" />
<text x="63" y="444" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="442" width="117" height="17" fill="#005f00" />
<text x="108" y="444" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine18 = </text>
<rect x="225" y="442" width="36" height="17" fill="#005f00" />
<text x="225" y="444" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="442" width="9" height="17" fill="#005f00" />
<text x="261" y="444" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="459" width="18" height="17" fill="#5f0000" />
<text x="18" y="461" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">19</text>
<rect x="36" y="459" width="9" height="17" fill="#5f0000" />
<rect x="45" y="459" width="9" height="17" fill="#5f0000" />
<text x="45" y="461" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="459" width="9" height="17" fill="#5f0000" />
<rect x="63" y="459" width="45" height="17" fill="#5f0000" />
<text x="63" y="461" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="459" width="117" height="17" fill="#5f0000" />
<text x="108" y="461" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine19 = </text>
<rect x="225" y="459" width="36" height="17" fill="#5f0000" />
<text x="225" y="461" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="459" width="9" height="17" fill="#5f0000" />
<text x="261" y="461" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="476" width="18" height="17" fill="#005f00" />
<text x="18" y="478" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">19</text>
<rect x="36" y="476" width="9" height="17" fill="#005f00" />
<rect x="45" y="476" width="9" height="17" fill="#005f00" />
<text x="45" y="478" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="476" width="9" height="17" fill="#005f00" />
<rect x="63" y="476" width="45" height="17" fill="#005f00" />
<text x="63" y="478" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="476" width="117" height="17" fill="#005f00" />
<text x="108" y="478" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine19 = </text>
<rect x="225" y="476" width="36" height="17" fill="#005f00" />
<text x="225" y="478" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="476" width="9" height="17" fill="#005f00" />
<text x="261" y="478" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="493" width="18" height="17" fill="#5f0000" />
<text x="18" y="495" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">20</text>
<rect x="36" y="493" width="9" height="17" fill="#5f0000" />
<rect x="45" y="493" width="9" height="17" fill="#5f0000" />
<text x="45" y="495" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="54" y="493" width="9" height="17" fill="#5f0000" />
<rect x="63" y="493" width="45" height="17" fill="#5f0000" />
<text x="63" y="495" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="493" width="117" height="17" fill="#5f0000" />
<text x="108" y="495" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine20 = </text>
<rect x="225" y="493" width="36" height="17" fill="#5f0000" />
<text x="225" y="495" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="493" width="9" height="17" fill="#5f0000" />
<text x="261" y="495" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="510" width="18" height="17" fill="#005f00" />
<text x="18" y="512" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">20</text>
<rect x="36" y="510" width="9" height="17" fill="#005f00" />
<rect x="45" y="510" width="9" height="17" fill="#005f00" />
<text x="45" y="512" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="54" y="510" width="9" height="17" fill="#005f00" />
<rect x="63" y="510" width="45" height="17" fill="#005f00" />
<text x="63" y="512" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="108" y="510" width="117" height="17" fill="#005f00" />
<text x="108" y="512" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine20 = </text>
<rect x="225" y="510" width="36" height="17" fill="#005f00" />
<text x="225" y="512" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="261" y="510" width="9" height="17" fill="#005f00" />
<text x="261" y="512" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="711" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="529" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Apply this change?</text>
<text x="711" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="546" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="561" width="9" height="17" fill="#001a00" />
<text x="18" y="563" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="561" width="9" height="17" fill="#001a00" />
<rect x="36" y="561" width="18" height="17" fill="#001a00" />
<text x="36" y="563" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
<rect x="54" y="561" width="9" height="17" fill="#001a00" />
<rect x="63" y="561" width="90" height="17" fill="#001a00" />
<text x="63" y="563" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
<rect x="153" y="561" width="153" height="17" fill="#001a00" />
<text x="711" y="563" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="580" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="580" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">2.</text>
<text x="63" y="580" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Allow for this session</text>
<text x="711" y="580" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="597" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="597" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
<text x="63" y="597" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Modify with external editor</text>
<text x="711" y="597" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="614" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="614" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">4.</text>
<text x="63" y="614" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
<text x="711" y="614" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="631" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="631" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#ffffaf" textLength="720" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,156 @@
<svg xmlns="http://www.w3.org/2000/svg" width="740" height="683" viewBox="0 0 740 683">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="740" height="683" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#ffffaf" textLength="720" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#ffffaf" textLength="135" lengthAdjust="spacingAndGlyphs" font-weight="bold">Action Required</text>
<text x="648" y="19" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">2 of 3</text>
<text x="711" y="19" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="36" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="53" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs">?</text>
<text x="45" y="53" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">run_shell_command</text>
<text x="207" y="53" fill="#afafaf" textLength="207" lengthAdjust="spacingAndGlyphs">Executes a bash command</text>
<text x="711" y="53" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="87" fill="#afafaf" textLength="234" lengthAdjust="spacingAndGlyphs">... 24 hidden (Ctrl+O) ...</text>
<text x="711" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="104" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="104" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 25&quot;</text>
<text x="711" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="121" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="121" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 26&quot;</text>
<text x="711" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="138" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="138" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 27&quot;</text>
<text x="711" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="155" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="155" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 28&quot;</text>
<text x="711" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="172" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="172" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 29&quot;</text>
<text x="711" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="189" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="189" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 30&quot;</text>
<text x="711" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="206" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="206" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 31&quot;</text>
<text x="711" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="223" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="223" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 32&quot;</text>
<text x="711" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="240" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="240" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 33&quot;</text>
<text x="711" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="257" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="257" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 34&quot;</text>
<text x="711" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="274" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="274" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 35&quot;</text>
<text x="711" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="291" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="291" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 36&quot;</text>
<text x="711" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="308" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="308" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 37&quot;</text>
<text x="711" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="325" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="325" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 38&quot;</text>
<text x="711" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="342" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="342" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 39&quot;</text>
<text x="711" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="359" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="359" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 40&quot;</text>
<text x="711" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="376" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="376" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 41&quot;</text>
<text x="711" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="393" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="393" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 42&quot;</text>
<text x="711" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="410" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="410" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 43&quot;</text>
<text x="711" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="427" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="427" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 44&quot;</text>
<text x="711" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="444" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="444" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 45&quot;</text>
<text x="711" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="461" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="461" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 46&quot;</text>
<text x="711" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="478" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="478" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 47&quot;</text>
<text x="711" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="495" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="495" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 48&quot;</text>
<text x="711" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="512" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="512" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 49&quot;</text>
<text x="711" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="529" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="63" y="529" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 50&quot;</text>
<text x="711" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="546" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Allow execution of: &apos;echo&apos;?</text>
<text x="711" y="546" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="563" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="580" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="18" y="578" width="9" height="17" fill="#001a00" />
<text x="18" y="580" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="578" width="9" height="17" fill="#001a00" />
<rect x="36" y="578" width="18" height="17" fill="#001a00" />
<text x="36" y="580" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
<rect x="54" y="578" width="9" height="17" fill="#001a00" />
<rect x="63" y="578" width="90" height="17" fill="#001a00" />
<text x="63" y="580" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
<rect x="153" y="578" width="135" height="17" fill="#001a00" />
<text x="711" y="580" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="597" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="597" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">2.</text>
<text x="63" y="597" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Allow for this session</text>
<text x="711" y="597" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="614" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="614" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
<text x="63" y="614" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
<text x="711" y="614" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="631" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="711" y="631" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#ffffaf" textLength="720" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -16,7 +16,6 @@ exports[`ToolConfirmationQueue > calculates availableContentHeight based on avai
│ 4. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
Press Ctrl+O to show more lines
"
`;
@@ -42,6 +41,130 @@ exports[`ToolConfirmationQueue > does not render expansion hint when constrainHe
"
`;
exports[`ToolConfirmationQueue > height allocation and layout > should handle security warning height correctly 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Action Required 3 of 3 │
│ │
│ ? run_shell_command Executes a bash command with a deceptive URL │
│ │
│ ... 6 hidden (Ctrl+O) ... │
│ echo "Line 37" │
│ echo "Line 38" │
│ echo "Line 39" │
│ echo "Line 40" │
│ echo "Line 41" │
│ echo "Line 42" │
│ echo "Line 43" │
│ echo "Line 44" │
│ echo "Line 45" │
│ echo "Line 46" │
│ echo "Line 47" │
│ echo "Line 48" │
│ echo "Line 49" │
│ echo "Line 50" │
│ curl https://täst.com │
│ │
│ ⚠ Warning: Deceptive URL(s) detected: │
│ │
│ Original: https://täst.com/ │
│ Actual Host (Punycode): https://xn--tst-qla.com/ │
│ │
│ Allow execution of: 'echo'? │
│ │
│ ● 1. Allow once │
│ 2. Allow for this session │
│ 3. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`ToolConfirmationQueue > height allocation and layout > should render the full queue wrapper with borders and content for large edit diffs 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Action Required │
│ │
│ ? replace Replaces content in a file │
│ │
│ ... 15 hidden (Ctrl+O) ... │
│ 8 + const newLine8 = true; │
│ 9 - const oldLine9 = true; │
│ 9 + const newLine9 = true; │
│ 10 - const oldLine10 = true; │
│ 10 + const newLine10 = true; │
│ 11 - const oldLine11 = true; │
│ 11 + const newLine11 = true; │
│ 12 - const oldLine12 = true; │
│ 12 + const newLine12 = true; │
│ 13 - const oldLine13 = true; │
│ 13 + const newLine13 = true; │
│ 14 - const oldLine14 = true; │
│ 14 + const newLine14 = true; │
│ 15 - const oldLine15 = true; │
│ 15 + const newLine15 = true; │
│ 16 - const oldLine16 = true; │
│ 16 + const newLine16 = true; │
│ 17 - const oldLine17 = true; │
│ 17 + const newLine17 = true; │
│ 18 - const oldLine18 = true; │
│ 18 + const newLine18 = true; │
│ 19 - const oldLine19 = true; │
│ 19 + const newLine19 = true; │
│ 20 - const oldLine20 = true; │
│ 20 + const newLine20 = true; │
│ Apply this change? │
│ │
│ ● 1. Allow once │
│ 2. Allow for this session │
│ 3. Modify with external editor │
│ 4. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`ToolConfirmationQueue > height allocation and layout > should render the full queue wrapper with borders and content for large exec commands 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Action Required 2 of 3 │
│ │
│ ? run_shell_command Executes a bash command │
│ │
│ ... 24 hidden (Ctrl+O) ... │
│ echo "Line 25" │
│ echo "Line 26" │
│ echo "Line 27" │
│ echo "Line 28" │
│ echo "Line 29" │
│ echo "Line 30" │
│ echo "Line 31" │
│ echo "Line 32" │
│ echo "Line 33" │
│ echo "Line 34" │
│ echo "Line 35" │
│ echo "Line 36" │
│ echo "Line 37" │
│ echo "Line 38" │
│ echo "Line 39" │
│ echo "Line 40" │
│ echo "Line 41" │
│ echo "Line 42" │
│ echo "Line 43" │
│ echo "Line 44" │
│ echo "Line 45" │
│ echo "Line 46" │
│ echo "Line 47" │
│ echo "Line 48" │
│ echo "Line 49" │
│ echo "Line 50" │
│ Allow execution of: 'echo'? │
│ │
│ ● 1. Allow once │
│ 2. Allow for this session │
│ 3. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`ToolConfirmationQueue > provides more height for ask_user by subtracting less overhead 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Answer Questions │
@@ -91,26 +214,6 @@ exports[`ToolConfirmationQueue > renders ExitPlanMode tool confirmation with Suc
"
`;
exports[`ToolConfirmationQueue > renders expansion hint when content is long and constrained 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Action Required │
│ │
│ ? replace edit file │
│ │
│ ... 49 hidden (Ctrl+O) ... │
│ 50 line │
│ Apply this change? │
│ │
│ ● 1. Allow once │
│ 2. Allow for this session │
│ 3. Modify with external editor │
│ 4. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
Press Ctrl+O to show more lines
"
`;
exports[`ToolConfirmationQueue > renders the confirming tool with progress indicator 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Action Required 1 of 3 │
@@ -232,7 +232,7 @@ describe('ToolConfirmationMessage', () => {
unmount();
});
it('should render multiline shell scripts with correct newlines and syntax highlighting (SVG snapshot)', async () => {
it('should render multiline shell scripts with correct newlines and syntax highlighting', async () => {
const confirmationDetails: SerializableConfirmationDetails = {
type: 'exec',
title: 'Confirm Multiline Script',
@@ -628,6 +628,83 @@ describe('ToolConfirmationMessage', () => {
unmount();
});
describe('height allocation and layout', () => {
it('should expand to available height for large exec commands', async () => {
let largeCommand = '';
for (let i = 1; i <= 50; i++) {
largeCommand += `echo "Line ${i}"\n`;
}
const confirmationDetails: SerializableConfirmationDetails = {
type: 'exec',
title: 'Confirm Execution',
command: largeCommand.trimEnd(),
rootCommand: 'echo',
rootCommands: ['echo'],
};
const { waitUntilReady, lastFrame, generateSvg, unmount } =
await renderWithProviders(
<ToolConfirmationMessage
callId="test-call-id"
confirmationDetails={confirmationDetails}
config={mockConfig}
getPreferredEditor={vi.fn()}
availableTerminalHeight={40}
terminalWidth={80}
/>,
);
await waitUntilReady();
const outputLines = lastFrame().split('\n');
// Should use the entire terminal height minus 1 line for the "Press Ctrl+O to show more lines" hint
expect(outputLines.length).toBe(39);
await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot();
unmount();
});
it('should expand to available height for large edit diffs', async () => {
// Create a large diff string
let largeDiff = '--- a/file.ts\n+++ b/file.ts\n@@ -1,10 +1,15 @@\n';
for (let i = 1; i <= 20; i++) {
largeDiff += `-const oldLine${i} = true;\n`;
largeDiff += `+const newLine${i} = true;\n`;
}
const confirmationDetails: SerializableConfirmationDetails = {
type: 'edit',
title: 'Confirm Edit',
fileName: 'file.ts',
filePath: '/file.ts',
fileDiff: largeDiff,
originalContent: 'old',
newContent: 'new',
isModifying: false,
};
const { waitUntilReady, lastFrame, generateSvg, unmount } =
await renderWithProviders(
<ToolConfirmationMessage
callId="test-call-id"
confirmationDetails={confirmationDetails}
config={mockConfig}
getPreferredEditor={vi.fn()}
availableTerminalHeight={40}
terminalWidth={80}
/>,
);
await waitUntilReady();
const outputLines = lastFrame().split('\n');
// Should use the entire terminal height minus 1 line for the "Press Ctrl+O to show more lines" hint
expect(outputLines.length).toBe(39);
await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot();
unmount();
});
});
describe('ESCAPE key behavior', () => {
beforeEach(() => {
vi.useFakeTimers();
@@ -5,8 +5,8 @@
*/
import type React from 'react';
import { useEffect, useMemo, useCallback, useState } from 'react';
import { Box, Text } from 'ink';
import { useEffect, useMemo, useCallback, useState, useRef } from 'react';
import { Box, Text, ResizeObserver, type DOMElement } from 'ink';
import { DiffRenderer } from './DiffRenderer.js';
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
import {
@@ -85,6 +85,64 @@ export const ToolConfirmationMessage: React.FC<
? mcpDetailsExpansionState.expanded
: false;
const [measuredSecurityWarningsHeight, setMeasuredSecurityWarningsHeight] =
useState(0);
const observerRef = useRef<ResizeObserver | null>(null);
const deceptiveUrlWarnings = useMemo(() => {
const urls: string[] = [];
if (confirmationDetails.type === 'info' && confirmationDetails.urls) {
urls.push(...confirmationDetails.urls);
} else if (confirmationDetails.type === 'exec') {
const commands =
confirmationDetails.commands && confirmationDetails.commands.length > 0
? confirmationDetails.commands
: [confirmationDetails.command];
for (const cmd of commands) {
const matches = cmd.match(/https?:\/\/[^\s"'`<>;&|()]+/g);
if (matches) urls.push(...matches);
}
}
const uniqueUrls = Array.from(new Set(urls));
return uniqueUrls
.map(getDeceptiveUrlDetails)
.filter((d): d is DeceptiveUrlDetails => d !== null);
}, [confirmationDetails]);
const deceptiveUrlWarningText = useMemo(() => {
if (deceptiveUrlWarnings.length === 0) return null;
return `**Warning:** Deceptive URL(s) detected:\n\n${deceptiveUrlWarnings
.map(
(w) =>
` **Original:** ${w.originalUrl}\n **Actual Host (Punycode):** ${w.punycodeUrl}`,
)
.join('\n\n')}`;
}, [deceptiveUrlWarnings]);
const onSecurityWarningsRefChange = useCallback((node: DOMElement | null) => {
if (observerRef.current) {
observerRef.current.disconnect();
observerRef.current = null;
}
if (node) {
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry) {
const newHeight = Math.round(entry.contentRect.height);
setMeasuredSecurityWarningsHeight((prev) =>
newHeight !== prev ? newHeight : prev,
);
}
});
observer.observe(node);
observerRef.current = observer;
} else {
setMeasuredSecurityWarningsHeight((prev) => (prev !== 0 ? 0 : prev));
}
}, []);
const settings = useSettings();
const allowPermanentApproval =
settings.merged.security.enablePermanentToolApproval &&
@@ -216,37 +274,6 @@ export const ToolConfirmationMessage: React.FC<
[handleConfirm],
);
const deceptiveUrlWarnings = useMemo(() => {
const urls: string[] = [];
if (confirmationDetails.type === 'info' && confirmationDetails.urls) {
urls.push(...confirmationDetails.urls);
} else if (confirmationDetails.type === 'exec') {
const commands =
confirmationDetails.commands && confirmationDetails.commands.length > 0
? confirmationDetails.commands
: [confirmationDetails.command];
for (const cmd of commands) {
const matches = cmd.match(/https?:\/\/[^\s"'`<>;&|()]+/g);
if (matches) urls.push(...matches);
}
}
const uniqueUrls = Array.from(new Set(urls));
return uniqueUrls
.map(getDeceptiveUrlDetails)
.filter((d): d is DeceptiveUrlDetails => d !== null);
}, [confirmationDetails]);
const deceptiveUrlWarningText = useMemo(() => {
if (deceptiveUrlWarnings.length === 0) return null;
return `**Warning:** Deceptive URL(s) detected:\n\n${deceptiveUrlWarnings
.map(
(w) =>
` **Original:** ${w.originalUrl}\n **Actual Host (Punycode):** ${w.punycodeUrl}`,
)
.join('\n\n')}`;
}, [deceptiveUrlWarnings]);
const getOptions = useCallback(() => {
const options: Array<RadioSelectItem<ToolConfirmationOutcome>> = [];
@@ -389,23 +416,36 @@ export const ToolConfirmationMessage: React.FC<
// Calculate the vertical space (in lines) consumed by UI elements
// surrounding the main body content.
const PADDING_OUTER_Y = 2; // Main container has `padding={1}` (top & bottom).
const MARGIN_BODY_BOTTOM = 1; // margin on the body container.
const PADDING_OUTER_Y = 1; // Main container has `paddingBottom={1}`.
const HEIGHT_QUESTION = 1; // The question text is one line.
const MARGIN_QUESTION_BOTTOM = 1; // Margin on the question container.
const SECURITY_WARNING_BOTTOM_MARGIN = 1; // Margin on the securityWarnings container.
const SHOW_MORE_LINES_HEIGHT = 1; // The "Press Ctrl+O to show more lines" hint.
const optionsCount = getOptions().length;
// The measured height includes the margin inside WarningMessage (1 line).
// We also add 1 line for the marginBottom on the securityWarnings container.
const securityWarningsHeight = deceptiveUrlWarningText
? measuredSecurityWarningsHeight + SECURITY_WARNING_BOTTOM_MARGIN
: 0;
const surroundingElementsHeight =
PADDING_OUTER_Y +
MARGIN_BODY_BOTTOM +
HEIGHT_QUESTION +
MARGIN_QUESTION_BOTTOM +
SHOW_MORE_LINES_HEIGHT +
optionsCount +
1; // Reserve one line for 'ShowMoreLines' hint
securityWarningsHeight;
return Math.max(availableTerminalHeight - surroundingElementsHeight, 1);
}, [availableTerminalHeight, getOptions, handlesOwnUI]);
}, [
availableTerminalHeight,
handlesOwnUI,
getOptions,
measuredSecurityWarningsHeight,
deceptiveUrlWarningText,
]);
const { question, bodyContent, options, securityWarnings, initialIndex } =
useMemo<{
@@ -547,10 +587,6 @@ export const ToolConfirmationMessage: React.FC<
let bodyContentHeight = availableBodyContentHeight();
let warnings: React.ReactNode = null;
if (bodyContentHeight !== undefined) {
bodyContentHeight -= 2; // Account for padding;
}
if (containsRedirection) {
// Calculate lines needed for Note and Tip
const safeWidth = Math.max(terminalWidth, 1);
@@ -759,7 +795,11 @@ export const ToolConfirmationMessage: React.FC<
</Box>
{securityWarnings && (
<Box flexShrink={0} marginBottom={1}>
<Box
flexShrink={0}
marginBottom={1}
ref={onSecurityWarningsRefChange}
>
{securityWarnings}
</Box>
)}
@@ -0,0 +1,468 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="666" viewBox="0 0 920 666">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="666" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#afafaf" textLength="405" lengthAdjust="spacingAndGlyphs">... first 9 lines hidden (Ctrl+O to show) ...</text>
<rect x="0" y="17" width="9" height="17" fill="#005f00" />
<rect x="9" y="17" width="9" height="17" fill="#005f00" />
<text x="9" y="19" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">5</text>
<rect x="18" y="17" width="9" height="17" fill="#005f00" />
<rect x="27" y="17" width="9" height="17" fill="#005f00" />
<text x="27" y="19" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="17" width="9" height="17" fill="#005f00" />
<rect x="45" y="17" width="45" height="17" fill="#005f00" />
<text x="45" y="19" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="17" width="108" height="17" fill="#005f00" />
<text x="90" y="19" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> newLine5 = </text>
<rect x="198" y="17" width="36" height="17" fill="#005f00" />
<text x="198" y="19" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="234" y="17" width="9" height="17" fill="#005f00" />
<text x="234" y="19" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="34" width="9" height="17" fill="#5f0000" />
<rect x="9" y="34" width="9" height="17" fill="#5f0000" />
<text x="9" y="36" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">6</text>
<rect x="18" y="34" width="9" height="17" fill="#5f0000" />
<rect x="27" y="34" width="9" height="17" fill="#5f0000" />
<text x="27" y="36" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="34" width="9" height="17" fill="#5f0000" />
<rect x="45" y="34" width="45" height="17" fill="#5f0000" />
<text x="45" y="36" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="34" width="108" height="17" fill="#5f0000" />
<text x="90" y="36" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> oldLine6 = </text>
<rect x="198" y="34" width="36" height="17" fill="#5f0000" />
<text x="198" y="36" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="234" y="34" width="9" height="17" fill="#5f0000" />
<text x="234" y="36" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="51" width="9" height="17" fill="#005f00" />
<rect x="9" y="51" width="9" height="17" fill="#005f00" />
<text x="9" y="53" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">6</text>
<rect x="18" y="51" width="9" height="17" fill="#005f00" />
<rect x="27" y="51" width="9" height="17" fill="#005f00" />
<text x="27" y="53" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="51" width="9" height="17" fill="#005f00" />
<rect x="45" y="51" width="45" height="17" fill="#005f00" />
<text x="45" y="53" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="51" width="108" height="17" fill="#005f00" />
<text x="90" y="53" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> newLine6 = </text>
<rect x="198" y="51" width="36" height="17" fill="#005f00" />
<text x="198" y="53" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="234" y="51" width="9" height="17" fill="#005f00" />
<text x="234" y="53" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="68" width="9" height="17" fill="#5f0000" />
<rect x="9" y="68" width="9" height="17" fill="#5f0000" />
<text x="9" y="70" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">7</text>
<rect x="18" y="68" width="9" height="17" fill="#5f0000" />
<rect x="27" y="68" width="9" height="17" fill="#5f0000" />
<text x="27" y="70" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="68" width="9" height="17" fill="#5f0000" />
<rect x="45" y="68" width="45" height="17" fill="#5f0000" />
<text x="45" y="70" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="68" width="108" height="17" fill="#5f0000" />
<text x="90" y="70" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> oldLine7 = </text>
<rect x="198" y="68" width="36" height="17" fill="#5f0000" />
<text x="198" y="70" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="234" y="68" width="9" height="17" fill="#5f0000" />
<text x="234" y="70" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="85" width="9" height="17" fill="#005f00" />
<rect x="9" y="85" width="9" height="17" fill="#005f00" />
<text x="9" y="87" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">7</text>
<rect x="18" y="85" width="9" height="17" fill="#005f00" />
<rect x="27" y="85" width="9" height="17" fill="#005f00" />
<text x="27" y="87" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="85" width="9" height="17" fill="#005f00" />
<rect x="45" y="85" width="45" height="17" fill="#005f00" />
<text x="45" y="87" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="85" width="108" height="17" fill="#005f00" />
<text x="90" y="87" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> newLine7 = </text>
<rect x="198" y="85" width="36" height="17" fill="#005f00" />
<text x="198" y="87" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="234" y="85" width="9" height="17" fill="#005f00" />
<text x="234" y="87" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="102" width="9" height="17" fill="#5f0000" />
<rect x="9" y="102" width="9" height="17" fill="#5f0000" />
<text x="9" y="104" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">8</text>
<rect x="18" y="102" width="9" height="17" fill="#5f0000" />
<rect x="27" y="102" width="9" height="17" fill="#5f0000" />
<text x="27" y="104" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="102" width="9" height="17" fill="#5f0000" />
<rect x="45" y="102" width="45" height="17" fill="#5f0000" />
<text x="45" y="104" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="102" width="108" height="17" fill="#5f0000" />
<text x="90" y="104" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> oldLine8 = </text>
<rect x="198" y="102" width="36" height="17" fill="#5f0000" />
<text x="198" y="104" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="234" y="102" width="9" height="17" fill="#5f0000" />
<text x="234" y="104" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="119" width="9" height="17" fill="#005f00" />
<rect x="9" y="119" width="9" height="17" fill="#005f00" />
<text x="9" y="121" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">8</text>
<rect x="18" y="119" width="9" height="17" fill="#005f00" />
<rect x="27" y="119" width="9" height="17" fill="#005f00" />
<text x="27" y="121" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="119" width="9" height="17" fill="#005f00" />
<rect x="45" y="119" width="45" height="17" fill="#005f00" />
<text x="45" y="121" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="119" width="108" height="17" fill="#005f00" />
<text x="90" y="121" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> newLine8 = </text>
<rect x="198" y="119" width="36" height="17" fill="#005f00" />
<text x="198" y="121" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="234" y="119" width="9" height="17" fill="#005f00" />
<text x="234" y="121" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="136" width="9" height="17" fill="#5f0000" />
<rect x="9" y="136" width="9" height="17" fill="#5f0000" />
<text x="9" y="138" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">9</text>
<rect x="18" y="136" width="9" height="17" fill="#5f0000" />
<rect x="27" y="136" width="9" height="17" fill="#5f0000" />
<text x="27" y="138" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="136" width="9" height="17" fill="#5f0000" />
<rect x="45" y="136" width="45" height="17" fill="#5f0000" />
<text x="45" y="138" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="136" width="108" height="17" fill="#5f0000" />
<text x="90" y="138" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> oldLine9 = </text>
<rect x="198" y="136" width="36" height="17" fill="#5f0000" />
<text x="198" y="138" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="234" y="136" width="9" height="17" fill="#5f0000" />
<text x="234" y="138" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="153" width="9" height="17" fill="#005f00" />
<rect x="9" y="153" width="9" height="17" fill="#005f00" />
<text x="9" y="155" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">9</text>
<rect x="18" y="153" width="9" height="17" fill="#005f00" />
<rect x="27" y="153" width="9" height="17" fill="#005f00" />
<text x="27" y="155" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="153" width="9" height="17" fill="#005f00" />
<rect x="45" y="153" width="45" height="17" fill="#005f00" />
<text x="45" y="155" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="153" width="108" height="17" fill="#005f00" />
<text x="90" y="155" fill="#e5e5e5" textLength="108" lengthAdjust="spacingAndGlyphs"> newLine9 = </text>
<rect x="198" y="153" width="36" height="17" fill="#005f00" />
<text x="198" y="155" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="234" y="153" width="9" height="17" fill="#005f00" />
<text x="234" y="155" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="170" width="18" height="17" fill="#5f0000" />
<text x="0" y="172" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<rect x="18" y="170" width="9" height="17" fill="#5f0000" />
<rect x="27" y="170" width="9" height="17" fill="#5f0000" />
<text x="27" y="172" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="170" width="9" height="17" fill="#5f0000" />
<rect x="45" y="170" width="45" height="17" fill="#5f0000" />
<text x="45" y="172" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="170" width="117" height="17" fill="#5f0000" />
<text x="90" y="172" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine10 = </text>
<rect x="207" y="170" width="36" height="17" fill="#5f0000" />
<text x="207" y="172" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="170" width="9" height="17" fill="#5f0000" />
<text x="243" y="172" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="187" width="18" height="17" fill="#005f00" />
<text x="0" y="189" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<rect x="18" y="187" width="9" height="17" fill="#005f00" />
<rect x="27" y="187" width="9" height="17" fill="#005f00" />
<text x="27" y="189" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="187" width="9" height="17" fill="#005f00" />
<rect x="45" y="187" width="45" height="17" fill="#005f00" />
<text x="45" y="189" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="187" width="117" height="17" fill="#005f00" />
<text x="90" y="189" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine10 = </text>
<rect x="207" y="187" width="36" height="17" fill="#005f00" />
<text x="207" y="189" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="187" width="9" height="17" fill="#005f00" />
<text x="243" y="189" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="204" width="18" height="17" fill="#5f0000" />
<text x="0" y="206" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">11</text>
<rect x="18" y="204" width="9" height="17" fill="#5f0000" />
<rect x="27" y="204" width="9" height="17" fill="#5f0000" />
<text x="27" y="206" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="204" width="9" height="17" fill="#5f0000" />
<rect x="45" y="204" width="45" height="17" fill="#5f0000" />
<text x="45" y="206" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="204" width="117" height="17" fill="#5f0000" />
<text x="90" y="206" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine11 = </text>
<rect x="207" y="204" width="36" height="17" fill="#5f0000" />
<text x="207" y="206" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="204" width="9" height="17" fill="#5f0000" />
<text x="243" y="206" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="221" width="18" height="17" fill="#005f00" />
<text x="0" y="223" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">11</text>
<rect x="18" y="221" width="9" height="17" fill="#005f00" />
<rect x="27" y="221" width="9" height="17" fill="#005f00" />
<text x="27" y="223" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="221" width="9" height="17" fill="#005f00" />
<rect x="45" y="221" width="45" height="17" fill="#005f00" />
<text x="45" y="223" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="221" width="117" height="17" fill="#005f00" />
<text x="90" y="223" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine11 = </text>
<rect x="207" y="221" width="36" height="17" fill="#005f00" />
<text x="207" y="223" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="221" width="9" height="17" fill="#005f00" />
<text x="243" y="223" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="238" width="18" height="17" fill="#5f0000" />
<text x="0" y="240" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">12</text>
<rect x="18" y="238" width="9" height="17" fill="#5f0000" />
<rect x="27" y="238" width="9" height="17" fill="#5f0000" />
<text x="27" y="240" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="238" width="9" height="17" fill="#5f0000" />
<rect x="45" y="238" width="45" height="17" fill="#5f0000" />
<text x="45" y="240" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="238" width="117" height="17" fill="#5f0000" />
<text x="90" y="240" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine12 = </text>
<rect x="207" y="238" width="36" height="17" fill="#5f0000" />
<text x="207" y="240" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="238" width="9" height="17" fill="#5f0000" />
<text x="243" y="240" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="255" width="18" height="17" fill="#005f00" />
<text x="0" y="257" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">12</text>
<rect x="18" y="255" width="9" height="17" fill="#005f00" />
<rect x="27" y="255" width="9" height="17" fill="#005f00" />
<text x="27" y="257" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="255" width="9" height="17" fill="#005f00" />
<rect x="45" y="255" width="45" height="17" fill="#005f00" />
<text x="45" y="257" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="255" width="117" height="17" fill="#005f00" />
<text x="90" y="257" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine12 = </text>
<rect x="207" y="255" width="36" height="17" fill="#005f00" />
<text x="207" y="257" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="255" width="9" height="17" fill="#005f00" />
<text x="243" y="257" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="272" width="18" height="17" fill="#5f0000" />
<text x="0" y="274" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">13</text>
<rect x="18" y="272" width="9" height="17" fill="#5f0000" />
<rect x="27" y="272" width="9" height="17" fill="#5f0000" />
<text x="27" y="274" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="272" width="9" height="17" fill="#5f0000" />
<rect x="45" y="272" width="45" height="17" fill="#5f0000" />
<text x="45" y="274" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="272" width="117" height="17" fill="#5f0000" />
<text x="90" y="274" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine13 = </text>
<rect x="207" y="272" width="36" height="17" fill="#5f0000" />
<text x="207" y="274" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="272" width="9" height="17" fill="#5f0000" />
<text x="243" y="274" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="289" width="18" height="17" fill="#005f00" />
<text x="0" y="291" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">13</text>
<rect x="18" y="289" width="9" height="17" fill="#005f00" />
<rect x="27" y="289" width="9" height="17" fill="#005f00" />
<text x="27" y="291" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="289" width="9" height="17" fill="#005f00" />
<rect x="45" y="289" width="45" height="17" fill="#005f00" />
<text x="45" y="291" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="289" width="117" height="17" fill="#005f00" />
<text x="90" y="291" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine13 = </text>
<rect x="207" y="289" width="36" height="17" fill="#005f00" />
<text x="207" y="291" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="289" width="9" height="17" fill="#005f00" />
<text x="243" y="291" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="306" width="18" height="17" fill="#5f0000" />
<text x="0" y="308" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">14</text>
<rect x="18" y="306" width="9" height="17" fill="#5f0000" />
<rect x="27" y="306" width="9" height="17" fill="#5f0000" />
<text x="27" y="308" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="306" width="9" height="17" fill="#5f0000" />
<rect x="45" y="306" width="45" height="17" fill="#5f0000" />
<text x="45" y="308" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="306" width="117" height="17" fill="#5f0000" />
<text x="90" y="308" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine14 = </text>
<rect x="207" y="306" width="36" height="17" fill="#5f0000" />
<text x="207" y="308" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="306" width="9" height="17" fill="#5f0000" />
<text x="243" y="308" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="323" width="18" height="17" fill="#005f00" />
<text x="0" y="325" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">14</text>
<rect x="18" y="323" width="9" height="17" fill="#005f00" />
<rect x="27" y="323" width="9" height="17" fill="#005f00" />
<text x="27" y="325" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="323" width="9" height="17" fill="#005f00" />
<rect x="45" y="323" width="45" height="17" fill="#005f00" />
<text x="45" y="325" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="323" width="117" height="17" fill="#005f00" />
<text x="90" y="325" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine14 = </text>
<rect x="207" y="323" width="36" height="17" fill="#005f00" />
<text x="207" y="325" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="323" width="9" height="17" fill="#005f00" />
<text x="243" y="325" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="340" width="18" height="17" fill="#5f0000" />
<text x="0" y="342" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">15</text>
<rect x="18" y="340" width="9" height="17" fill="#5f0000" />
<rect x="27" y="340" width="9" height="17" fill="#5f0000" />
<text x="27" y="342" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="340" width="9" height="17" fill="#5f0000" />
<rect x="45" y="340" width="45" height="17" fill="#5f0000" />
<text x="45" y="342" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="340" width="117" height="17" fill="#5f0000" />
<text x="90" y="342" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine15 = </text>
<rect x="207" y="340" width="36" height="17" fill="#5f0000" />
<text x="207" y="342" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="340" width="9" height="17" fill="#5f0000" />
<text x="243" y="342" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="357" width="18" height="17" fill="#005f00" />
<text x="0" y="359" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">15</text>
<rect x="18" y="357" width="9" height="17" fill="#005f00" />
<rect x="27" y="357" width="9" height="17" fill="#005f00" />
<text x="27" y="359" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="357" width="9" height="17" fill="#005f00" />
<rect x="45" y="357" width="45" height="17" fill="#005f00" />
<text x="45" y="359" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="357" width="117" height="17" fill="#005f00" />
<text x="90" y="359" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine15 = </text>
<rect x="207" y="357" width="36" height="17" fill="#005f00" />
<text x="207" y="359" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="357" width="9" height="17" fill="#005f00" />
<text x="243" y="359" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="374" width="18" height="17" fill="#5f0000" />
<text x="0" y="376" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">16</text>
<rect x="18" y="374" width="9" height="17" fill="#5f0000" />
<rect x="27" y="374" width="9" height="17" fill="#5f0000" />
<text x="27" y="376" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="374" width="9" height="17" fill="#5f0000" />
<rect x="45" y="374" width="45" height="17" fill="#5f0000" />
<text x="45" y="376" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="374" width="117" height="17" fill="#5f0000" />
<text x="90" y="376" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine16 = </text>
<rect x="207" y="374" width="36" height="17" fill="#5f0000" />
<text x="207" y="376" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="374" width="9" height="17" fill="#5f0000" />
<text x="243" y="376" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="391" width="18" height="17" fill="#005f00" />
<text x="0" y="393" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">16</text>
<rect x="18" y="391" width="9" height="17" fill="#005f00" />
<rect x="27" y="391" width="9" height="17" fill="#005f00" />
<text x="27" y="393" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="391" width="9" height="17" fill="#005f00" />
<rect x="45" y="391" width="45" height="17" fill="#005f00" />
<text x="45" y="393" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="391" width="117" height="17" fill="#005f00" />
<text x="90" y="393" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine16 = </text>
<rect x="207" y="391" width="36" height="17" fill="#005f00" />
<text x="207" y="393" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="391" width="9" height="17" fill="#005f00" />
<text x="243" y="393" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="408" width="18" height="17" fill="#5f0000" />
<text x="0" y="410" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">17</text>
<rect x="18" y="408" width="9" height="17" fill="#5f0000" />
<rect x="27" y="408" width="9" height="17" fill="#5f0000" />
<text x="27" y="410" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="408" width="9" height="17" fill="#5f0000" />
<rect x="45" y="408" width="45" height="17" fill="#5f0000" />
<text x="45" y="410" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="408" width="117" height="17" fill="#5f0000" />
<text x="90" y="410" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine17 = </text>
<rect x="207" y="408" width="36" height="17" fill="#5f0000" />
<text x="207" y="410" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="408" width="9" height="17" fill="#5f0000" />
<text x="243" y="410" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="425" width="18" height="17" fill="#005f00" />
<text x="0" y="427" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">17</text>
<rect x="18" y="425" width="9" height="17" fill="#005f00" />
<rect x="27" y="425" width="9" height="17" fill="#005f00" />
<text x="27" y="427" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="425" width="9" height="17" fill="#005f00" />
<rect x="45" y="425" width="45" height="17" fill="#005f00" />
<text x="45" y="427" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="425" width="117" height="17" fill="#005f00" />
<text x="90" y="427" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine17 = </text>
<rect x="207" y="425" width="36" height="17" fill="#005f00" />
<text x="207" y="427" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="425" width="9" height="17" fill="#005f00" />
<text x="243" y="427" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="442" width="18" height="17" fill="#5f0000" />
<text x="0" y="444" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">18</text>
<rect x="18" y="442" width="9" height="17" fill="#5f0000" />
<rect x="27" y="442" width="9" height="17" fill="#5f0000" />
<text x="27" y="444" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="442" width="9" height="17" fill="#5f0000" />
<rect x="45" y="442" width="45" height="17" fill="#5f0000" />
<text x="45" y="444" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="442" width="117" height="17" fill="#5f0000" />
<text x="90" y="444" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine18 = </text>
<rect x="207" y="442" width="36" height="17" fill="#5f0000" />
<text x="207" y="444" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="442" width="9" height="17" fill="#5f0000" />
<text x="243" y="444" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="459" width="18" height="17" fill="#005f00" />
<text x="0" y="461" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">18</text>
<rect x="18" y="459" width="9" height="17" fill="#005f00" />
<rect x="27" y="459" width="9" height="17" fill="#005f00" />
<text x="27" y="461" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="459" width="9" height="17" fill="#005f00" />
<rect x="45" y="459" width="45" height="17" fill="#005f00" />
<text x="45" y="461" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="459" width="117" height="17" fill="#005f00" />
<text x="90" y="461" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine18 = </text>
<rect x="207" y="459" width="36" height="17" fill="#005f00" />
<text x="207" y="461" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="459" width="9" height="17" fill="#005f00" />
<text x="243" y="461" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="476" width="18" height="17" fill="#5f0000" />
<text x="0" y="478" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">19</text>
<rect x="18" y="476" width="9" height="17" fill="#5f0000" />
<rect x="27" y="476" width="9" height="17" fill="#5f0000" />
<text x="27" y="478" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="476" width="9" height="17" fill="#5f0000" />
<rect x="45" y="476" width="45" height="17" fill="#5f0000" />
<text x="45" y="478" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="476" width="117" height="17" fill="#5f0000" />
<text x="90" y="478" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine19 = </text>
<rect x="207" y="476" width="36" height="17" fill="#5f0000" />
<text x="207" y="478" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="476" width="9" height="17" fill="#5f0000" />
<text x="243" y="478" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="493" width="18" height="17" fill="#005f00" />
<text x="0" y="495" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">19</text>
<rect x="18" y="493" width="9" height="17" fill="#005f00" />
<rect x="27" y="493" width="9" height="17" fill="#005f00" />
<text x="27" y="495" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="493" width="9" height="17" fill="#005f00" />
<rect x="45" y="493" width="45" height="17" fill="#005f00" />
<text x="45" y="495" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="493" width="117" height="17" fill="#005f00" />
<text x="90" y="495" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine19 = </text>
<rect x="207" y="493" width="36" height="17" fill="#005f00" />
<text x="207" y="495" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="493" width="9" height="17" fill="#005f00" />
<text x="243" y="495" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="510" width="18" height="17" fill="#5f0000" />
<text x="0" y="512" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">20</text>
<rect x="18" y="510" width="9" height="17" fill="#5f0000" />
<rect x="27" y="510" width="9" height="17" fill="#5f0000" />
<text x="27" y="512" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
<rect x="36" y="510" width="9" height="17" fill="#5f0000" />
<rect x="45" y="510" width="45" height="17" fill="#5f0000" />
<text x="45" y="512" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="510" width="117" height="17" fill="#5f0000" />
<text x="90" y="512" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> oldLine20 = </text>
<rect x="207" y="510" width="36" height="17" fill="#5f0000" />
<text x="207" y="512" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="510" width="9" height="17" fill="#5f0000" />
<text x="243" y="512" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<rect x="0" y="527" width="18" height="17" fill="#005f00" />
<text x="0" y="529" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">20</text>
<rect x="18" y="527" width="9" height="17" fill="#005f00" />
<rect x="27" y="527" width="9" height="17" fill="#005f00" />
<text x="27" y="529" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
<rect x="36" y="527" width="9" height="17" fill="#005f00" />
<rect x="45" y="527" width="45" height="17" fill="#005f00" />
<text x="45" y="529" fill="#0000ee" textLength="45" lengthAdjust="spacingAndGlyphs">const</text>
<rect x="90" y="527" width="117" height="17" fill="#005f00" />
<text x="90" y="529" fill="#e5e5e5" textLength="117" lengthAdjust="spacingAndGlyphs"> newLine20 = </text>
<rect x="207" y="527" width="36" height="17" fill="#005f00" />
<text x="207" y="529" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<rect x="243" y="527" width="9" height="17" fill="#005f00" />
<text x="243" y="529" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="0" y="546" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Apply this change?</text>
<rect x="0" y="578" width="9" height="17" fill="#001a00" />
<text x="0" y="580" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="9" y="578" width="9" height="17" fill="#001a00" />
<rect x="18" y="578" width="18" height="17" fill="#001a00" />
<text x="18" y="580" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
<rect x="36" y="578" width="9" height="17" fill="#001a00" />
<rect x="45" y="578" width="90" height="17" fill="#001a00" />
<text x="45" y="580" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
<rect x="135" y="578" width="153" height="17" fill="#001a00" />
<text x="18" y="597" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">2.</text>
<text x="45" y="597" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Allow for this session</text>
<text x="18" y="614" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
<text x="45" y="614" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Modify with external editor</text>
<text x="18" y="631" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">4.</text>
<text x="45" y="631" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 36 KiB

@@ -0,0 +1,87 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="666" viewBox="0 0 920 666">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="666" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#afafaf" textLength="414" lengthAdjust="spacingAndGlyphs">... first 18 lines hidden (Ctrl+O to show) ...</text>
<text x="0" y="19" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="19" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 19&quot;</text>
<text x="0" y="36" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="36" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 20&quot;</text>
<text x="0" y="53" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="53" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 21&quot;</text>
<text x="0" y="70" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="70" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 22&quot;</text>
<text x="0" y="87" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="87" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 23&quot;</text>
<text x="0" y="104" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="104" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 24&quot;</text>
<text x="0" y="121" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="121" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 25&quot;</text>
<text x="0" y="138" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="138" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 26&quot;</text>
<text x="0" y="155" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="155" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 27&quot;</text>
<text x="0" y="172" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="172" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 28&quot;</text>
<text x="0" y="189" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="189" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 29&quot;</text>
<text x="0" y="206" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="206" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 30&quot;</text>
<text x="0" y="223" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="223" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 31&quot;</text>
<text x="0" y="240" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="240" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 32&quot;</text>
<text x="0" y="257" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="257" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 33&quot;</text>
<text x="0" y="274" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="274" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 34&quot;</text>
<text x="0" y="291" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="291" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 35&quot;</text>
<text x="0" y="308" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="308" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 36&quot;</text>
<text x="0" y="325" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="325" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 37&quot;</text>
<text x="0" y="342" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="342" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 38&quot;</text>
<text x="0" y="359" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="359" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 39&quot;</text>
<text x="0" y="376" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="376" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 40&quot;</text>
<text x="0" y="393" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="393" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 41&quot;</text>
<text x="0" y="410" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="410" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 42&quot;</text>
<text x="0" y="427" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="427" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 43&quot;</text>
<text x="0" y="444" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="444" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 44&quot;</text>
<text x="0" y="461" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="461" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 45&quot;</text>
<text x="0" y="478" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="478" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 46&quot;</text>
<text x="0" y="495" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="495" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 47&quot;</text>
<text x="0" y="512" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="512" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 48&quot;</text>
<text x="0" y="529" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="529" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 49&quot;</text>
<text x="0" y="546" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">echo</text>
<text x="45" y="546" fill="#cdcd00" textLength="81" lengthAdjust="spacingAndGlyphs">&quot;Line 50&quot;</text>
<text x="0" y="563" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Allow execution of: &apos;echo&apos;?</text>
<rect x="0" y="595" width="9" height="17" fill="#001a00" />
<text x="0" y="597" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="9" y="595" width="9" height="17" fill="#001a00" />
<rect x="18" y="595" width="18" height="17" fill="#001a00" />
<text x="18" y="597" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
<rect x="36" y="595" width="9" height="17" fill="#001a00" />
<rect x="45" y="595" width="90" height="17" fill="#001a00" />
<text x="45" y="597" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
<rect x="135" y="595" width="135" height="17" fill="#001a00" />
<text x="18" y="614" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">2.</text>
<text x="45" y="614" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Allow for this session</text>
<text x="18" y="631" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
<text x="45" y="631" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

@@ -16,6 +16,90 @@ Apply this change?
"
`;
exports[`ToolConfirmationMessage > height allocation and layout > should expand to available height for large edit diffs 1`] = `
"... first 9 lines hidden (Ctrl+O to show) ...
5 + const newLine5 = true;
6 - const oldLine6 = true;
6 + const newLine6 = true;
7 - const oldLine7 = true;
7 + const newLine7 = true;
8 - const oldLine8 = true;
8 + const newLine8 = true;
9 - const oldLine9 = true;
9 + const newLine9 = true;
10 - const oldLine10 = true;
10 + const newLine10 = true;
11 - const oldLine11 = true;
11 + const newLine11 = true;
12 - const oldLine12 = true;
12 + const newLine12 = true;
13 - const oldLine13 = true;
13 + const newLine13 = true;
14 - const oldLine14 = true;
14 + const newLine14 = true;
15 - const oldLine15 = true;
15 + const newLine15 = true;
16 - const oldLine16 = true;
16 + const newLine16 = true;
17 - const oldLine17 = true;
17 + const newLine17 = true;
18 - const oldLine18 = true;
18 + const newLine18 = true;
19 - const oldLine19 = true;
19 + const newLine19 = true;
20 - const oldLine20 = true;
20 + const newLine20 = true;
Apply this change?
● 1. Allow once
2. Allow for this session
3. Modify with external editor
4. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > height allocation and layout > should expand to available height for large exec commands 1`] = `
"... first 18 lines hidden (Ctrl+O to show) ...
echo "Line 19"
echo "Line 20"
echo "Line 21"
echo "Line 22"
echo "Line 23"
echo "Line 24"
echo "Line 25"
echo "Line 26"
echo "Line 27"
echo "Line 28"
echo "Line 29"
echo "Line 30"
echo "Line 31"
echo "Line 32"
echo "Line 33"
echo "Line 34"
echo "Line 35"
echo "Line 36"
echo "Line 37"
echo "Line 38"
echo "Line 39"
echo "Line 40"
echo "Line 41"
echo "Line 42"
echo "Line 43"
echo "Line 44"
echo "Line 45"
echo "Line 46"
echo "Line 47"
echo "Line 48"
echo "Line 49"
echo "Line 50"
Allow execution of: 'echo'?
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
`;
exports[`ToolConfirmationMessage > should display multiple commands for exec type when provided 1`] = `
"echo "hello"
@@ -53,7 +137,7 @@ Do you want to proceed?
"
`;
exports[`ToolConfirmationMessage > should render multiline shell scripts with correct newlines and syntax highlighting (SVG snapshot) 1`] = `
exports[`ToolConfirmationMessage > should render multiline shell scripts with correct newlines and syntax highlighting 1`] = `
"echo "hello"
for i in 1 2 3; do
echo $i
@@ -10,6 +10,7 @@ import { useUIState } from '../../contexts/UIStateContext.js';
import { ExtensionUpdateState } from '../../state/extensions.js';
import { debugLogger, type GeminiCLIExtension } from '@google/gemini-cli-core';
import { getFormattedSettingValue } from '../../../commands/extensions/utils.js';
import { theme } from '../../semantic-colors.js';
interface ExtensionsList {
extensions: readonly GeminiCLIExtension[];
@@ -61,11 +62,29 @@ export const ExtensionsList: React.FC<ExtensionsList> = ({ extensions }) => {
return (
<Box key={ext.name} flexDirection="column" marginBottom={1}>
<Text>
<Text color="cyan">{`${ext.name} (v${ext.version})`}</Text>
<Text color={activeColor}>{` - ${activeString}`}</Text>
{<Text color={stateColor}>{` (${stateText})`}</Text>}
</Text>
<Box flexDirection="row">
<Text>
<Text color="cyan">{`${ext.name} (v${ext.version})`}</Text>
<Text color={activeColor}>{` - ${activeString}`}</Text>
<Text color={stateColor}>{` (${stateText})`}</Text>
</Text>
{ext.manifestType === 'open-plugin' && (
<Box marginLeft={1}>
<Text
backgroundColor={theme.ui.dark}
color={theme.text.secondary}
>
{' '}
Plugin{' '}
</Text>
</Box>
)}
</Box>
{ext.description && (
<Box paddingLeft={2}>
<Text color="gray">{ext.description}</Text>
</Box>
)}
{ext.resolvedSettings && ext.resolvedSettings.length > 0 && (
<Box flexDirection="column" paddingLeft={2}>
<Text>settings:</Text>
@@ -1,32 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="207" viewBox="0 0 920 207">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="343" viewBox="0 0 920 343">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="207" fill="#000000" />
<rect width="920" height="343" fill="#000000" />
<g transform="translate(10, 10)">
<text x="18" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="180" y="19" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.2.3</text>
<text x="36" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="121" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">google_web_search</text>
<text x="855" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="155" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Searching...</text>
<text x="855" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="9" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="19" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛</text>
<text x="27" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="36" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">█▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌</text>
<text x="18" y="53" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="53" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌</text>
<text x="9" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="70" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀</text>
<text x="9" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="99" y="104" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.2.3</text>
<text x="0" y="155" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="172" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="172" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="172" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="189" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="189" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="189" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="206" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="223" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
<text x="0" y="240" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="257" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">google_web_search</text>
<text x="855" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="291" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Searching...</text>
<text x="855" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -1,32 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="207" viewBox="0 0 920 207">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="343" viewBox="0 0 920 343">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="207" fill="#000000" />
<rect width="920" height="343" fill="#000000" />
<g transform="translate(10, 10)">
<text x="18" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="180" y="19" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.2.3</text>
<text x="36" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#87afff" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="121" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="121" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="121" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">run_shell_command</text>
<text x="855" y="121" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="138" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="155" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Running command...</text>
<text x="855" y="155" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#87afff" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="9" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="19" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛</text>
<text x="27" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="36" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">█▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌</text>
<text x="18" y="53" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="53" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌</text>
<text x="9" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="70" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀</text>
<text x="9" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="99" y="104" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.2.3</text>
<text x="0" y="155" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="172" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="172" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="172" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="189" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="189" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="189" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="206" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="223" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
<text x="0" y="240" fill="#87afff" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="257" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="257" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="257" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">run_shell_command</text>
<text x="855" y="257" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="274" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="291" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Running command...</text>
<text x="855" y="291" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#87afff" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -1,32 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="207" viewBox="0 0 920 207">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="343" viewBox="0 0 920 343">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="207" fill="#000000" />
<rect width="920" height="343" fill="#000000" />
<g transform="translate(10, 10)">
<text x="18" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="180" y="19" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.2.3</text>
<text x="36" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="121" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">google_web_search</text>
<text x="855" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="155" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Searching...</text>
<text x="855" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="9" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="19" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛</text>
<text x="27" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="36" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">█▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌</text>
<text x="18" y="53" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="53" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs">▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌</text>
<text x="9" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="70" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀</text>
<text x="9" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="99" y="104" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.2.3</text>
<text x="0" y="155" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="172" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="172" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="172" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="189" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="189" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="189" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="206" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="223" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
<text x="0" y="240" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="257" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">google_web_search</text>
<text x="855" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="291" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Searching...</text>
<text x="855" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -2,11 +2,19 @@
exports[`MainContent tool group border SVG snapshots > should render SVG snapshot for a pending search dialog (google_web_search) 1`] = `
"
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.2.3
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊶ google_web_search │
│ │
@@ -16,11 +24,19 @@ exports[`MainContent tool group border SVG snapshots > should render SVG snapsho
exports[`MainContent tool group border SVG snapshots > should render SVG snapshot for a shell tool 1`] = `
"
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.2.3
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊶ run_shell_command │
│ │
@@ -30,11 +46,19 @@ exports[`MainContent tool group border SVG snapshots > should render SVG snapsho
exports[`MainContent tool group border SVG snapshots > should render SVG snapshot for an empty slice following a search tool 1`] = `
"
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.2.3
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊶ google_web_search │
│ │
@@ -13,12 +13,14 @@ import {
disableModifyOtherKeys,
enableBracketedPasteMode,
disableBracketedPasteMode,
disableMouseEvents,
} from '@google/gemini-cli-core';
import { parseColor } from '../themes/color-utils.js';
export type TerminalBackgroundColor = string | undefined;
const TERMINAL_CLEANUP_SEQUENCE = '\x1b[<u\x1b[>4;0m\x1b[?2004l';
const TERMINAL_CLEANUP_SEQUENCE =
'\x1b[<u\x1b[>4;0m\x1b[?2004l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l';
export function cleanupTerminalOnExit() {
try {
@@ -33,6 +35,7 @@ export function cleanupTerminalOnExit() {
disableKittyKeyboardProtocol();
disableModifyOtherKeys();
disableBracketedPasteMode();
disableMouseEvents();
}
export class TerminalCapabilityManager {
@@ -502,7 +502,6 @@ export function useTerminalSetupPrompt({
if (hasBeenPrompted) {
return;
}
let cancelled = false;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
+40
View File
@@ -72,6 +72,46 @@ describe('cleanup', () => {
expect(asyncFn).toHaveBeenCalledTimes(1);
});
it('should run cleanupFunctions BEFORE draining stdin and BEFORE runSyncCleanup', async () => {
const callOrder: string[] = [];
// Cleanup function
registerCleanup(() => {
callOrder.push('cleanup');
});
// Sync cleanup function (e.g. setRawMode(false))
registerSyncCleanup(() => {
callOrder.push('sync');
});
// Mock stdin.resume to track drainStdin
const originalResume = process.stdin.resume;
process.stdin.resume = vi.fn().mockImplementation(() => {
callOrder.push('drain');
return process.stdin;
});
// Mock stdin properties for drainStdin
const originalIsTTY = process.stdin.isTTY;
Object.defineProperty(process.stdin, 'isTTY', {
value: true,
configurable: true,
});
try {
await runExitCleanup();
} finally {
process.stdin.resume = originalResume;
Object.defineProperty(process.stdin, 'isTTY', {
value: originalIsTTY,
configurable: true,
});
}
expect(callOrder).toEqual(['drain', 'drain', 'sync', 'cleanup']);
});
it('should continue running cleanup functions even if one throws an error', async () => {
const errorFn = vi.fn().mockImplementation(() => {
throw new Error('test error');
+1 -1
View File
@@ -59,7 +59,7 @@ export function registerTelemetryConfig(config: Config) {
export async function runExitCleanup() {
// drain stdin to prevent printing garbage on exit
// https://github.com/google-gemini/gemini-cli/issues/1680
// https://github.com/google-gemini/gemini-cli/issues/16801
await drainStdin();
runSyncCleanup();
@@ -117,6 +117,7 @@ describe('AgentSession', () => {
expect(events).toHaveLength(0);
expect(protocol.events).toHaveLength(1);
expect(protocol.events[0].type).toBe('session_update');
expect(protocol.events[0].streamId).toEqual(expect.any(String));
});
it('should skip events that occur before agent_start', async () => {
@@ -171,6 +172,181 @@ describe('AgentSession', () => {
expect(streamedEvents).toEqual(allEvents.slice(2));
});
it('should complete immediately when resuming from agent_end', async () => {
const protocol = new MockAgentProtocol();
const session = new AgentSession(protocol);
protocol.pushResponse([{ type: 'message' }]);
const { streamId } = await session.send({
message: [{ type: 'text', text: 'request' }],
});
await new Promise((resolve) => setTimeout(resolve, 10));
const endEvent = session.events.findLast(
(event): event is AgentEvent<'agent_end'> =>
event.type === 'agent_end' && event.streamId === streamId,
);
expect(endEvent).toBeDefined();
const iterator = session
.stream({ eventId: endEvent!.id })
[Symbol.asyncIterator]();
await expect(iterator.next()).resolves.toEqual({
value: undefined,
done: true,
});
});
it('should throw for an unknown eventId', async () => {
const protocol = new MockAgentProtocol();
const session = new AgentSession(protocol);
const iterator = session
.stream({ eventId: 'missing-event' })
[Symbol.asyncIterator]();
await expect(iterator.next()).rejects.toThrow(
'Unknown eventId: missing-event',
);
});
it('should throw when resuming from an event before agent_start on a stream with no agent activity', async () => {
const protocol = new MockAgentProtocol();
const session = new AgentSession(protocol);
const { streamId } = await session.send({ update: { title: 'draft' } });
expect(streamId).toBeNull();
const updateEvent = session.events.find(
(event): event is AgentEvent<'session_update'> =>
event.type === 'session_update',
);
expect(updateEvent).toBeDefined();
const iterator = session
.stream({ eventId: updateEvent!.id })
[Symbol.asyncIterator]();
await expect(iterator.next()).rejects.toThrow(
`Cannot resume from eventId ${updateEvent!.id} before agent_start for stream ${updateEvent!.streamId}`,
);
});
it('should replay from agent_start when resuming from a pre-agent_start event after activity is in history', async () => {
const protocol = new MockAgentProtocol();
const session = new AgentSession(protocol);
protocol.pushResponse([
{
type: 'message',
role: 'agent',
content: [{ type: 'text', text: 'hello' }],
},
]);
await session.send({
message: [{ type: 'text', text: 'request' }],
});
await new Promise((resolve) => setTimeout(resolve, 10));
const userMessage = session.events.find(
(event): event is AgentEvent<'message'> =>
event.type === 'message' && event.role === 'user',
);
expect(userMessage).toBeDefined();
const streamedEvents: AgentEvent[] = [];
for await (const event of session.stream({ eventId: userMessage!.id })) {
streamedEvents.push(event);
}
expect(streamedEvents.map((event) => event.type)).toEqual([
'agent_start',
'message',
'agent_end',
]);
expect(streamedEvents[0]?.streamId).toBe(userMessage!.streamId);
});
it('should throw when resuming from a pre-agent_start event before activity is in history', async () => {
const protocol = new MockAgentProtocol([
{
id: 'e-1',
timestamp: '2026-01-01T00:00:00.000Z',
streamId: 'stream-1',
type: 'message',
role: 'user',
content: [{ type: 'text', text: 'request' }],
},
]);
const session = new AgentSession(protocol);
const iterator = session
.stream({ eventId: 'e-1' })
[Symbol.asyncIterator]();
await expect(iterator.next()).rejects.toThrow(
'Cannot resume from eventId e-1 before agent_start for stream stream-1',
);
});
it('should resume from an in-stream event within the same stream only', async () => {
const protocol = new MockAgentProtocol();
const session = new AgentSession(protocol);
protocol.pushResponse([
{
type: 'message',
role: 'agent',
content: [{ type: 'text', text: 'first answer 1' }],
},
{
type: 'message',
role: 'agent',
content: [{ type: 'text', text: 'first answer 2' }],
},
]);
const { streamId: streamId1 } = await session.send({
message: [{ type: 'text', text: 'first request' }],
});
await new Promise((resolve) => setTimeout(resolve, 10));
protocol.pushResponse([
{
type: 'message',
role: 'agent',
content: [{ type: 'text', text: 'second answer' }],
},
]);
await session.send({
message: [{ type: 'text', text: 'second request' }],
});
await new Promise((resolve) => setTimeout(resolve, 10));
const resumeEvent = session.events.find(
(event): event is AgentEvent<'message'> =>
event.type === 'message' &&
event.streamId === streamId1 &&
event.role === 'agent' &&
event.content[0]?.type === 'text' &&
event.content[0].text === 'first answer 1',
);
expect(resumeEvent).toBeDefined();
const streamedEvents: AgentEvent[] = [];
for await (const event of session.stream({ eventId: resumeEvent!.id })) {
streamedEvents.push(event);
}
expect(
streamedEvents.every((event) => event.streamId === streamId1),
).toBe(true);
expect(streamedEvents.map((event) => event.type)).toEqual([
'message',
'agent_end',
]);
const resumedMessage = streamedEvents[0] as AgentEvent<'message'>;
expect(resumedMessage.content).toEqual([
{ type: 'text', text: 'first answer 2' },
]);
});
it('should replay events for streamId starting with agent_start', async () => {
const protocol = new MockAgentProtocol();
const session = new AgentSession(protocol);
@@ -223,6 +399,33 @@ describe('AgentSession', () => {
expect(streamedEvents.at(-1)?.type).toBe('agent_end');
});
it('should not drop agent_end that arrives while replay events are being yielded', async () => {
const protocol = new MockAgentProtocol();
const session = new AgentSession(protocol);
protocol.pushResponse([{ type: 'message' }], { keepOpen: true });
const { streamId } = await session.send({ update: { title: 't1' } });
await new Promise((resolve) => setTimeout(resolve, 10));
const iterator = session
.stream({ streamId: streamId! })
[Symbol.asyncIterator]();
const first = await iterator.next();
expect(first.value?.type).toBe('agent_start');
protocol.pushToStream(streamId!, [], { close: true });
const second = await iterator.next();
expect(second.value?.type).toBe('message');
const third = await iterator.next();
expect(third.value?.type).toBe('agent_end');
const fourth = await iterator.next();
expect(fourth.done).toBe(true);
});
it('should follow an active stream if no options provided', async () => {
const protocol = new MockAgentProtocol();
const session = new AgentSession(protocol);
+74 -61
View File
@@ -34,7 +34,7 @@ export class AgentSession implements AgentProtocol {
return this._protocol.abort();
}
get events(): AgentEvent[] {
get events(): readonly AgentEvent[] {
return this._protocol.events;
}
@@ -77,6 +77,30 @@ export class AgentSession implements AgentProtocol {
let done = false;
let trackedStreamId = options.streamId;
let started = false;
let agentActivityStarted = false;
const queueVisibleEvent = (event: AgentEvent): void => {
if (trackedStreamId && event.streamId !== trackedStreamId) {
return;
}
if (!agentActivityStarted) {
if (event.type !== 'agent_start') {
return;
}
trackedStreamId = event.streamId;
agentActivityStarted = true;
}
if (!trackedStreamId) {
return;
}
eventQueue.push(event);
if (event.type === 'agent_end' && event.streamId === trackedStreamId) {
done = true;
}
};
// 1. Subscribe early to avoid missing any events that occur during replay setup
const unsubscribe = this._protocol.subscribe((event) => {
@@ -87,23 +111,7 @@ export class AgentSession implements AgentProtocol {
return;
}
if (trackedStreamId && event.streamId !== trackedStreamId) return;
// If we don't have a tracked stream yet, the first agent_start we see becomes it.
if (!trackedStreamId && event.type === 'agent_start') {
trackedStreamId = event.streamId ?? undefined;
}
// If we still don't have a tracked stream and we aren't replaying everything (eventId), ignore.
if (!trackedStreamId && !options.eventId) return;
eventQueue.push(event);
if (
event.type === 'agent_end' &&
event.streamId === (trackedStreamId ?? null)
) {
done = true;
}
queueVisibleEvent(event);
const currentResolve = resolve;
next = new Promise<void>((r) => {
@@ -118,8 +126,42 @@ export class AgentSession implements AgentProtocol {
if (options.eventId) {
const index = currentEvents.findIndex((e) => e.id === options.eventId);
if (index !== -1) {
if (index === -1) {
throw new Error(`Unknown eventId: ${options.eventId}`);
}
const resumeEvent = currentEvents[index];
trackedStreamId = resumeEvent.streamId;
const firstAgentStartIndex = currentEvents.findIndex(
(event) =>
event.type === 'agent_start' && event.streamId === trackedStreamId,
);
if (resumeEvent.type === 'agent_end') {
replayStartIndex = index + 1;
agentActivityStarted = true;
done = true;
} else if (
firstAgentStartIndex !== -1 &&
firstAgentStartIndex <= index
) {
replayStartIndex = index + 1;
agentActivityStarted = true;
} else if (firstAgentStartIndex !== -1) {
// A pre-agent_start cursor can be resumed once the corresponding
// agent activity is already present in history. Because stream()
// yields only agent_start -> agent_end, replay begins at agent_start
// rather than at the original pre-start event.
replayStartIndex = firstAgentStartIndex;
} else {
// Consumers can only resume by eventId once the corresponding stream
// has entered the agent_start -> agent_end lifecycle in history.
// Without a recorded agent_start, this wrapper cannot distinguish
// "agent activity may start later" from "this send was acknowledged
// without agent activity" without risking an infinite wait.
throw new Error(
`Cannot resume from eventId ${options.eventId} before agent_start for stream ${trackedStreamId}`,
);
}
} else if (options.streamId) {
const index = currentEvents.findIndex(
@@ -128,29 +170,7 @@ export class AgentSession implements AgentProtocol {
if (index !== -1) {
replayStartIndex = index;
}
}
if (replayStartIndex !== -1) {
for (let i = replayStartIndex; i < currentEvents.length; i++) {
const event = currentEvents[i];
if (options.streamId && event.streamId !== options.streamId) continue;
eventQueue.push(event);
if (event.type === 'agent_start' && !trackedStreamId) {
trackedStreamId = event.streamId ?? undefined;
}
if (
event.type === 'agent_end' &&
event.streamId === (trackedStreamId ?? null)
) {
done = true;
break;
}
}
}
if (!done && !trackedStreamId) {
// Find active stream in history
} else {
const activeStarts = currentEvents.filter(
(e) => e.type === 'agent_start',
);
@@ -161,36 +181,28 @@ export class AgentSession implements AgentProtocol {
(e) => e.type === 'agent_end' && e.streamId === start.streamId,
)
) {
trackedStreamId = start.streamId ?? undefined;
trackedStreamId = start.streamId;
replayStartIndex = currentEvents.findIndex(
(e) => e.id === start.id,
);
break;
}
}
}
// If we replayed to the end and no stream is active, and we were specifically
// replaying from an eventId (or we've already finished the stream we were looking for), we are done.
if (!done && !trackedStreamId && options.eventId) {
done = true;
if (replayStartIndex !== -1) {
for (let i = replayStartIndex; i < currentEvents.length; i++) {
const event = currentEvents[i];
queueVisibleEvent(event);
if (done) break;
}
}
started = true;
// Process events that arrived while we were replaying
for (const event of earlyEvents) {
if (done) break;
if (trackedStreamId && event.streamId !== trackedStreamId) continue;
if (!trackedStreamId && event.type === 'agent_start') {
trackedStreamId = event.streamId ?? undefined;
}
if (!trackedStreamId && !options.eventId) continue;
eventQueue.push(event);
if (
event.type === 'agent_end' &&
event.streamId === (trackedStreamId ?? null)
) {
done = true;
}
queueVisibleEvent(event);
}
while (true) {
@@ -200,6 +212,7 @@ export class AgentSession implements AgentProtocol {
for (const event of eventsToYield) {
yield event;
}
continue;
}
if (done) break;
+1 -1
View File
@@ -235,7 +235,7 @@ describe('MockAgentProtocol', () => {
expect(streamId).toBeNull();
expect(session.events).toHaveLength(1);
expect(session.events[0].type).toBe('session_update');
expect(session.events[0].streamId).toBeNull();
expect(session.events[0].streamId).toEqual(expect.any(String));
});
it('should throw on action', async () => {
+42 -30
View File
@@ -8,8 +8,8 @@ import type {
AgentEvent,
AgentEventCommon,
AgentEventData,
AgentSend,
AgentProtocol,
AgentSend,
Unsubscribe,
} from './types.js';
@@ -86,13 +86,7 @@ export class MockAgentProtocol implements AgentProtocol {
) {
const now = new Date().toISOString();
for (const eventData of events) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const event: AgentEvent = {
...eventData,
id: eventData.id ?? `e-${this._nextEventId++}`,
timestamp: eventData.timestamp ?? now,
streamId: eventData.streamId ?? streamId,
} as AgentEvent;
const event = this._normalizeEvent(eventData, now, streamId);
this._emit(event);
}
@@ -100,13 +94,13 @@ export class MockAgentProtocol implements AgentProtocol {
options?.close &&
!events.some((eventData) => eventData.type === 'agent_end')
) {
this._emit({
id: `e-${this._nextEventId++}`,
timestamp: now,
streamId,
type: 'agent_end',
reason: 'completed',
} as AgentEvent);
this._emit(
this._normalizeEvent(
{ type: 'agent_end', reason: 'completed' },
now,
streamId,
),
);
}
}
@@ -124,16 +118,18 @@ export class MockAgentProtocol implements AgentProtocol {
const now = new Date().toISOString();
const eventsToEmit: AgentEvent[] = [];
let fallbackStreamId: string | undefined;
// Helper to normalize and prepare for emission
// All emitted events stay correlated to a stream even if this send does not
// start agent activity and therefore returns `streamId: null`.
const normalize = (eventData: MockAgentEvent): AgentEvent =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
({
...eventData,
id: eventData.id ?? `e-${this._nextEventId++}`,
timestamp: eventData.timestamp ?? now,
streamId: eventData.streamId ?? streamId,
}) as AgentEvent;
this._normalizeEvent(
eventData,
now,
eventData.streamId ??
streamId ??
(fallbackStreamId ??= `mock-stream-${this._nextStreamId++}`),
);
// 1. User/Update event (BEFORE agent_start)
if ('message' in payload && payload.message) {
@@ -225,16 +221,32 @@ export class MockAgentProtocol implements AgentProtocol {
return { streamId };
}
private _normalizeEvent(
eventData: MockAgentEvent,
timestamp: string,
streamId: string,
): AgentEvent {
// TypeScript loses the specific union member when we add common event
// fields here, so keep the narrowing local to this mock-only helper.
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return {
...eventData,
id: eventData.id ?? `e-${this._nextEventId++}`,
timestamp: eventData.timestamp ?? timestamp,
streamId: eventData.streamId ?? streamId,
} as AgentEvent;
}
async abort(): Promise<void> {
if (this._lastStreamId && this._activeStreamIds.has(this._lastStreamId)) {
const streamId = this._lastStreamId;
this._emit({
id: `e-${this._nextEventId++}`,
timestamp: new Date().toISOString(),
streamId,
type: 'agent_end',
reason: 'aborted',
} as AgentEvent);
this._emit(
this._normalizeEvent(
{ type: 'agent_end', reason: 'aborted' },
new Date().toISOString(),
streamId,
),
);
}
}
}
+8 -7
View File
@@ -11,9 +11,10 @@ export type Unsubscribe = () => void;
export interface AgentProtocol extends Trajectory {
/**
* Send data to the agent. Promise resolves when action is acknowledged.
* Returns the `streamId` of the stream the message was correlated to --
* this may be a new stream if idle, an existing stream, or null if no
* stream was triggered.
* Returns the agent-activity `streamId` affected by the send. This may be a
* new stream if idle, an existing stream, or null if the send was
* acknowledged without starting agent activity. Emitted events should still
* remain correlated to a stream via their `streamId`.
*
* When a new stream is created by a send, the streamId MUST be returned
* before the `agent_start` event is emitted for the stream.
@@ -36,7 +37,7 @@ export interface AgentProtocol extends Trajectory {
/**
* AgentProtocol implements the Trajectory interface and can retrieve existing events.
*/
readonly events: AgentEvent[];
readonly events: readonly AgentEvent[];
}
type RequireExactlyOne<T> = {
@@ -54,7 +55,7 @@ interface AgentSendPayloads {
export type AgentSend = RequireExactlyOne<AgentSendPayloads> & WithMeta;
export interface Trajectory {
readonly events: AgentEvent[];
readonly events: readonly AgentEvent[];
}
export interface AgentEventCommon {
@@ -62,8 +63,8 @@ export interface AgentEventCommon {
id: string;
/** Identifies the subagent thread, omitted for "main thread" events. */
threadId?: string;
/** Identifies a particular stream of a particular thread. */
streamId?: string | null;
/** Identifies the stream this event belongs to. */
streamId: string;
/** ISO Timestamp for the time at which the event occurred. */
timestamp: string;
/** The concrete type of the event. */
@@ -343,6 +343,32 @@ Body`);
expect(result.modelConfig.model).toBe('auto');
});
it('should expand ${PLUGIN_ROOT} in agent definition', () => {
const markdown = {
kind: 'local' as const,
name: 'expansion-agent',
description: 'An agent in ${PLUGIN_ROOT}',
system_prompt: 'You are at ${PLUGIN_ROOT}',
mcp_servers: {
'test-server': {
command: 'node',
args: ['${PLUGIN_ROOT}/server.js'],
},
},
};
const pluginRoot = '/abs/path/to/plugin';
const result = markdownToAgentDefinition(markdown, {
pluginRoot,
}) as LocalAgentDefinition;
expect(result.description).toBe(`An agent in ${pluginRoot}`);
expect(result.promptConfig.systemPrompt).toBe(`You are at ${pluginRoot}`);
expect(result.mcpServers!['test-server'].args).toEqual([
`${pluginRoot}/server.js`,
]);
});
it('should convert remote agent definition', () => {
const markdown = {
kind: 'remote' as const,
+69 -22
View File
@@ -489,17 +489,55 @@ function convertFrontmatterAuthToConfig(
}
}
/**
* Recursively expands ${PLUGIN_ROOT} in strings, arrays, and objects.
*/
function recursivelyExpandPluginRoot<T>(obj: T, pluginRoot: string): T {
if (typeof obj === 'string') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return obj.replace(/\${PLUGIN_ROOT}/g, pluginRoot) as unknown as T;
}
if (Array.isArray(obj)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return obj.map((item) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
recursivelyExpandPluginRoot(item, pluginRoot),
) as unknown as T;
}
if (typeof obj === 'object' && obj !== null) {
const newObj: Record<string, unknown> = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
newObj[key] = recursivelyExpandPluginRoot(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(obj as Record<string, unknown>)[key],
pluginRoot,
);
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return newObj as T;
}
return obj;
}
/**
* Converts a FrontmatterAgentDefinition DTO to the internal AgentDefinition structure.
*
* @param markdown The parsed Markdown/Frontmatter definition.
* @param metadata Optional metadata including hash and file path.
* @param metadata Optional metadata including hash, file path, and plugin root.
* @returns The internal AgentDefinition.
*/
export function markdownToAgentDefinition(
markdown: FrontmatterAgentDefinition,
metadata?: { hash?: string; filePath?: string },
metadata?: { hash?: string; filePath?: string; pluginRoot?: string },
): AgentDefinition {
const pluginRoot = metadata?.pluginRoot;
const processedMarkdown =
pluginRoot !== undefined
? recursivelyExpandPluginRoot(markdown, pluginRoot)
: markdown;
const inputConfig = {
inputSchema: {
type: 'object',
@@ -514,15 +552,15 @@ export function markdownToAgentDefinition(
},
};
if (markdown.kind === 'remote') {
if (processedMarkdown.kind === 'remote') {
return {
kind: 'remote',
name: markdown.name,
description: markdown.description || '',
displayName: markdown.display_name,
agentCardUrl: markdown.agent_card_url,
auth: markdown.auth
? convertFrontmatterAuthToConfig(markdown.auth)
name: processedMarkdown.name,
description: processedMarkdown.description || '',
displayName: processedMarkdown.display_name,
agentCardUrl: processedMarkdown.agent_card_url,
auth: processedMarkdown.auth
? convertFrontmatterAuthToConfig(processedMarkdown.auth)
: undefined,
inputConfig,
metadata,
@@ -530,11 +568,13 @@ export function markdownToAgentDefinition(
}
// If a model is specified, use it. Otherwise, inherit
const modelName = markdown.model || 'inherit';
const modelName = processedMarkdown.model || 'inherit';
const mcpServers: Record<string, MCPServerConfig> = {};
if (markdown.kind === 'local' && markdown.mcp_servers) {
for (const [name, config] of Object.entries(markdown.mcp_servers)) {
if (processedMarkdown.kind === 'local' && processedMarkdown.mcp_servers) {
for (const [name, config] of Object.entries(
processedMarkdown.mcp_servers,
)) {
mcpServers[name] = new MCPServerConfig(
config.command,
config.args,
@@ -556,27 +596,28 @@ export function markdownToAgentDefinition(
return {
kind: 'local',
name: markdown.name,
description: markdown.description,
displayName: markdown.display_name,
name: processedMarkdown.name,
description: processedMarkdown.description,
displayName: processedMarkdown.display_name,
promptConfig: {
systemPrompt: markdown.system_prompt,
systemPrompt: processedMarkdown.system_prompt,
query: '${query}',
},
modelConfig: {
model: modelName,
generateContentConfig: {
temperature: markdown.temperature ?? 1,
temperature: processedMarkdown.temperature ?? 1,
topP: 0.95,
},
},
runConfig: {
maxTurns: markdown.max_turns ?? DEFAULT_MAX_TURNS,
maxTimeMinutes: markdown.timeout_mins ?? DEFAULT_MAX_TIME_MINUTES,
maxTurns: processedMarkdown.max_turns ?? DEFAULT_MAX_TURNS,
maxTimeMinutes:
processedMarkdown.timeout_mins ?? DEFAULT_MAX_TIME_MINUTES,
},
toolConfig: markdown.tools
toolConfig: processedMarkdown.tools
? {
tools: markdown.tools,
tools: processedMarkdown.tools,
}
: undefined,
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
@@ -591,10 +632,12 @@ export function markdownToAgentDefinition(
* Supported extensions: .md
*
* @param dir Directory path to scan.
* @param pluginRoot Optional plugin root path for variable expansion.
* @returns Object containing successfully loaded agents and any errors.
*/
export async function loadAgentsFromDirectory(
dir: string,
pluginRoot?: string,
): Promise<AgentLoadResult> {
const result: AgentLoadResult = {
agents: [],
@@ -634,7 +677,11 @@ export async function loadAgentsFromDirectory(
const hash = crypto.createHash('sha256').update(content).digest('hex');
const agentDefs = await parseAgentMarkdown(filePath, content);
for (const def of agentDefs) {
const agent = markdownToAgentDefinition(def, { hash, filePath });
const agent = markdownToAgentDefinition(def, {
hash,
filePath,
pluginRoot,
});
result.agents.push(agent);
}
} catch (error) {
+26
View File
@@ -1206,6 +1206,32 @@ describe('AgentRegistry', () => {
});
describe('inheritance and refresh', () => {
it('should skip remote agents when refreshing on model change', async () => {
const remoteAgent: AgentDefinition = {
kind: 'remote',
name: 'RemoteAgent',
description: 'A remote agent',
agentCardUrl: 'https://example.com/card',
inputConfig: { inputSchema: { type: 'object' } },
};
const loadAgentSpy = vi.fn().mockResolvedValue({ name: 'RemoteAgent' });
vi.spyOn(mockConfig, 'getA2AClientManager').mockReturnValue({
loadAgent: loadAgentSpy,
clearCache: vi.fn(),
} as unknown as A2AClientManager);
await registry.testRegisterAgent(remoteAgent);
expect(loadAgentSpy).toHaveBeenCalledTimes(1);
coreEvents.emitModelChanged('new-model');
await new Promise((resolve) => setTimeout(resolve, 0));
expect(loadAgentSpy).toHaveBeenCalledTimes(1);
});
it('should resolve "inherit" to the current model from configuration', async () => {
const config = makeMockedConfig({ model: 'current-model' });
const registry = new TestableAgentRegistry(config);
+9 -5
View File
@@ -57,7 +57,7 @@ export class AgentRegistry {
}
private onModelChanged = () => {
this.refreshAgents().catch((e) => {
this.refreshAgents('local').catch((e) => {
debugLogger.error(
'[AgentRegistry] Failed to refresh agents on model change:',
e,
@@ -270,12 +270,16 @@ export class AgentRegistry {
}
}
private async refreshAgents(): Promise<void> {
private async refreshAgents(
scope: AgentDefinition['kind'] | 'all' = 'all',
): Promise<void> {
this.loadBuiltInAgents();
await Promise.allSettled(
Array.from(this.agents.values()).map((agent) =>
this.registerAgent(agent),
),
Array.from(this.agents.values()).map(async (agent) => {
if (scope === 'all' || agent.kind === scope) {
await this.registerAgent(agent);
}
}),
);
}
+1
View File
@@ -129,6 +129,7 @@ export interface BaseAgentDefinition<
metadata?: {
hash?: string;
filePath?: string;
pluginRoot?: string;
};
}
+1 -1
View File
@@ -14,7 +14,7 @@ import {
type ConversationOffered,
type StreamingLatency,
} from './types.js';
import type { CompletedToolCall } from '../core/coreToolScheduler.js';
import type { CompletedToolCall } from '../scheduler/types.js';
import type { Config } from '../config/config.js';
import { debugLogger } from '../utils/debugLogger.js';
import { getCodeAssistServer } from './codeAssist.js';
+5 -1
View File
@@ -348,6 +348,10 @@ export interface GeminiCLIExtension {
isActive: boolean;
path: string;
installMetadata?: ExtensionInstallMetadata;
manifestType?: 'gemini' | 'open-plugin';
description?: string;
author?: string | { name: string; email?: string; url?: string };
license?: string;
mcpServers?: Record<string, MCPServerConfig>;
contextFiles: string[];
excludeTools?: string[];
@@ -1001,7 +1005,7 @@ export class Config implements McpContext, AgentLoopContext {
this.model = params.model;
this.disableLoopDetection = params.disableLoopDetection ?? false;
this._activeModel = params.model;
this.enableAgents = params.enableAgents ?? true;
this.enableAgents = params.enableAgents ?? false;
this.agents = params.agents ?? {};
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
this.planEnabled = params.plan ?? true;
+1 -1
View File
@@ -32,7 +32,7 @@ import {
} from '../config/models.js';
import { hasCycleInSchema } from '../tools/tools.js';
import type { StructuredError } from './turn.js';
import type { CompletedToolCall } from './coreToolScheduler.js';
import type { CompletedToolCall } from '../scheduler/types.js';
import {
logContentRetry,
logContentRetryFailure,
-1
View File
@@ -43,7 +43,6 @@ export * from './core/prompts.js';
export * from './core/tokenLimits.js';
export * from './core/turn.js';
export * from './core/geminiRequest.js';
export * from './core/coreToolScheduler.js';
export * from './scheduler/scheduler.js';
export * from './scheduler/types.js';
export * from './scheduler/tool-executor.js';
@@ -0,0 +1,305 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { Scheduler } from './scheduler.js';
import type { ErroredToolCall } from './types.js';
import { CoreToolCallStatus } from './types.js';
import type { Config, ToolRegistry, AgentLoopContext } from '../index.js';
import {
ApprovalMode,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
} from '../index.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
import { MockTool } from '../test-utils/mock-tool.js';
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
import type { PolicyEngine } from '../policy/policy-engine.js';
import { HookSystem } from '../hooks/hookSystem.js';
import { HookType, HookEventName } from '../hooks/types.js';
function createMockConfig(overrides: Partial<Config> = {}): Config {
const defaultToolRegistry = {
getTool: () => undefined,
getToolByName: () => undefined,
getFunctionDeclarations: () => [],
tools: new Map(),
discovery: {},
registerTool: () => {},
getToolByDisplayName: () => undefined,
getTools: () => [],
discoverTools: async () => {},
getAllTools: () => [],
getToolsByServer: () => [],
getExperiments: () => {},
} as unknown as ToolRegistry;
const baseConfig = {
getSessionId: () => 'test-session-id',
getUsageStatisticsEnabled: () => true,
getDebugMode: () => false,
isInteractive: () => true,
getApprovalMode: () => ApprovalMode.DEFAULT,
setApprovalMode: () => {},
getAllowedTools: () => [],
getContentGeneratorConfig: () => ({
model: 'test-model',
authType: 'oauth-personal',
}),
getShellExecutionConfig: () => ({
terminalWidth: 90,
terminalHeight: 30,
sanitizationConfig: {
enableEnvironmentVariableRedaction: true,
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
},
}),
storage: {
getProjectTempDir: () => '/tmp',
},
getTruncateToolOutputThreshold: () =>
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
getTruncateToolOutputLines: () => 1000,
getToolRegistry: () => defaultToolRegistry,
getWorkingDir: () => '/mock/dir',
getActiveModel: () => DEFAULT_GEMINI_MODEL,
getGeminiClient: () => null,
getMessageBus: () => createMockMessageBus(),
getEnableHooks: () => true,
getExperiments: () => {},
getPolicyEngine: () =>
({
check: async () => ({ decision: 'allow' }),
}) as unknown as PolicyEngine,
} as unknown as Config;
const mockConfig = Object.assign({}, baseConfig, overrides) as Config;
(mockConfig as { config?: Config }).config = mockConfig;
return mockConfig;
}
describe('Scheduler Hooks', () => {
it('should stop execution if BeforeTool hook requests stop', async () => {
const executeFn = vi.fn().mockResolvedValue({
llmContent: 'Tool executed',
returnDisplay: 'Tool executed',
});
const mockTool = new MockTool({ name: 'mockTool', execute: executeFn });
const toolRegistry = {
getTool: () => mockTool,
getAllToolNames: () => ['mockTool'],
} as unknown as ToolRegistry;
const mockMessageBus = createMockMessageBus();
const mockConfig = createMockConfig({
getToolRegistry: () => toolRegistry,
getMessageBus: () => mockMessageBus,
getApprovalMode: () => ApprovalMode.YOLO,
});
const hookSystem = new HookSystem(mockConfig);
(mockConfig as { getHookSystem?: () => HookSystem }).getHookSystem = () =>
hookSystem;
// Register a programmatic runtime hook
hookSystem.registerHook(
{
type: HookType.Runtime,
name: 'test-stop-hook',
action: async () => ({
continue: false,
stopReason: 'Hook stopped execution',
}),
},
HookEventName.BeforeTool,
);
const scheduler = new Scheduler({
context: {
config: mockConfig,
messageBus: mockMessageBus,
toolRegistry,
} as unknown as AgentLoopContext,
getPreferredEditor: () => 'vscode',
schedulerId: 'test-scheduler',
});
const request = {
callId: '1',
name: 'mockTool',
args: {},
isClientInitiated: false,
prompt_id: 'prompt-1',
};
const results = await scheduler.schedule(
[request],
new AbortController().signal,
);
expect(results.length).toBe(1);
const result = results[0];
expect(result.status).toBe(CoreToolCallStatus.Error);
const erroredCall = result as ErroredToolCall;
expect(erroredCall.response.error?.message).toContain(
'Agent execution stopped by hook: Hook stopped execution',
);
expect(executeFn).not.toHaveBeenCalled();
});
it('should block tool execution if BeforeTool hook requests block', async () => {
const executeFn = vi.fn();
const mockTool = new MockTool({ name: 'mockTool', execute: executeFn });
const toolRegistry = {
getTool: () => mockTool,
getAllToolNames: () => ['mockTool'],
} as unknown as ToolRegistry;
const mockMessageBus = createMockMessageBus();
const mockConfig = createMockConfig({
getToolRegistry: () => toolRegistry,
getMessageBus: () => mockMessageBus,
getApprovalMode: () => ApprovalMode.YOLO,
});
const hookSystem = new HookSystem(mockConfig);
(mockConfig as { getHookSystem?: () => HookSystem }).getHookSystem = () =>
hookSystem;
hookSystem.registerHook(
{
type: HookType.Runtime,
name: 'test-block-hook',
action: async () => ({
decision: 'block',
reason: 'Hook blocked execution',
}),
},
HookEventName.BeforeTool,
);
const scheduler = new Scheduler({
context: {
config: mockConfig,
messageBus: mockMessageBus,
toolRegistry,
} as unknown as AgentLoopContext,
getPreferredEditor: () => 'vscode',
schedulerId: 'test-scheduler',
});
const request = {
callId: '1',
name: 'mockTool',
args: {},
isClientInitiated: false,
prompt_id: 'prompt-1',
};
const results = await scheduler.schedule(
[request],
new AbortController().signal,
);
expect(results.length).toBe(1);
const result = results[0];
expect(result.status).toBe(CoreToolCallStatus.Error);
const erroredCall = result as ErroredToolCall;
expect(erroredCall.response.error?.message).toContain(
'Tool execution blocked: Hook blocked execution',
);
expect(executeFn).not.toHaveBeenCalled();
});
it('should update tool input if BeforeTool hook provides modified input', async () => {
const executeFn = vi.fn().mockResolvedValue({
llmContent: 'Tool executed',
returnDisplay: 'Tool executed',
});
const mockTool = new MockTool({ name: 'mockTool', execute: executeFn });
const toolRegistry = {
getTool: () => mockTool,
getAllToolNames: () => ['mockTool'],
} as unknown as ToolRegistry;
const mockMessageBus = createMockMessageBus();
const mockConfig = createMockConfig({
getToolRegistry: () => toolRegistry,
getMessageBus: () => mockMessageBus,
getApprovalMode: () => ApprovalMode.YOLO,
});
const hookSystem = new HookSystem(mockConfig);
(mockConfig as { getHookSystem?: () => HookSystem }).getHookSystem = () =>
hookSystem;
hookSystem.registerHook(
{
type: HookType.Runtime,
name: 'test-modify-input-hook',
action: async () => ({
continue: true,
hookSpecificOutput: {
hookEventName: 'BeforeTool',
tool_input: { newParam: 'modifiedValue' },
},
}),
},
HookEventName.BeforeTool,
);
const scheduler = new Scheduler({
context: {
config: mockConfig,
messageBus: mockMessageBus,
toolRegistry,
} as unknown as AgentLoopContext,
getPreferredEditor: () => 'vscode',
schedulerId: 'test-scheduler',
});
const request = {
callId: '1',
name: 'mockTool',
args: { originalParam: 'originalValue' },
isClientInitiated: false,
prompt_id: 'prompt-1',
};
const results = await scheduler.schedule(
[request],
new AbortController().signal,
);
expect(results.length).toBe(1);
const result = results[0];
expect(result.status).toBe(CoreToolCallStatus.Success);
expect(executeFn).toHaveBeenCalledWith(
{ newParam: 'modifiedValue' },
expect.anything(),
undefined,
expect.anything(),
);
expect(result.request.args).toEqual({
newParam: 'modifiedValue',
});
});
});
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { type Status } from '../core/coreToolScheduler.js';
import { type Status } from '../scheduler/types.js';
import { type ThoughtSummary } from '../utils/thoughtUtils.js';
import { getProjectHash } from '../utils/paths.js';
import { sanitizeFilenamePart } from '../utils/fileUtils.js';
@@ -12,11 +12,11 @@ import { describe, it, expect } from 'vitest';
import { logToolCall } from './loggers.js';
import { ToolCallEvent } from './types.js';
import type { Config } from '../config/config.js';
import type { CompletedToolCall } from '../core/coreToolScheduler.js';
import {
CoreToolCallStatus,
type ToolCallRequestInfo,
type ToolCallResponseInfo,
type CompletedToolCall,
} from '../scheduler/types.js';
import { MockTool } from '../test-utils/mock-tool.js';
+1 -1
View File
@@ -13,7 +13,7 @@ import type {
import type { Config } from '../config/config.js';
import type { ApprovalMode } from '../policy/types.js';
import type { CompletedToolCall } from '../core/coreToolScheduler.js';
import type { CompletedToolCall } from '../scheduler/types.js';
import { CoreToolCallStatus } from '../scheduler/types.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
import { AuthType } from '../core/contentGenerator.js';
+10 -4
View File
@@ -212,14 +212,20 @@ export class McpClientManager {
*/
async startExtension(extension: GeminiCLIExtension) {
debugLogger.log(`Loading extension: ${extension.name}`);
const mcpServers = Object.entries(extension.mcpServers ?? {});
await Promise.all(
Object.entries(extension.mcpServers ?? {}).map(([name, config]) =>
this.maybeDiscoverMcpServer(name, {
mcpServers.map(([name, config]) => {
let serverName = name;
if (extension.manifestType === 'open-plugin') {
// Open Plugin MCP servers are prefixed with the plugin name using a colon.
serverName = `${extension.name}:${name}`;
}
return this.maybeDiscoverMcpServer(serverName, {
// eslint-disable-next-line @typescript-eslint/no-misused-spread
...config,
extension,
}),
),
});
}),
);
await this.scheduleMcpContextRefresh();
}
+17 -3
View File
@@ -2271,11 +2271,22 @@ export async function createTransport(
}
}
const expandedCommand = expandEnvVars(
mcpServerConfig.command,
expansionEnv,
);
const expandedArgs = (mcpServerConfig.args || []).map((arg) =>
expandEnvVars(arg, expansionEnv),
);
const expandedCwd = mcpServerConfig.cwd
? expandEnvVars(mcpServerConfig.cwd, expansionEnv)
: undefined;
let transport: Transport = new StdioClientTransport({
command: mcpServerConfig.command,
args: mcpServerConfig.args || [],
command: expandedCommand,
args: expandedArgs,
env: finalEnv,
cwd: mcpServerConfig.cwd,
cwd: expandedCwd,
stderr: 'pipe',
});
@@ -2329,6 +2340,9 @@ function getExtensionEnvironment(
extension?: GeminiCLIExtension,
): Record<string, string> {
const env: Record<string, string> = {};
if (extension?.path) {
env['PLUGIN_ROOT'] = extension.path;
}
if (extension?.resolvedSettings) {
for (const setting of extension.resolvedSettings) {
if (setting.value !== undefined) {
@@ -0,0 +1,98 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as SdkClientStdioLib from '@modelcontextprotocol/sdk/client/stdio.js';
import { createTransport, type McpContext } from './mcp-client.js';
import type { GeminiCLIExtension, MCPServerConfig } from '../config/config.js';
vi.mock('@modelcontextprotocol/sdk/client/stdio.js');
describe('MCP Plugin Variable Expansion', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const mockMcpContext: Partial<McpContext> = {
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
},
isTrustedFolder: () => true,
emitMcpDiagnostic: () => {},
};
it('should expand ${PLUGIN_ROOT} in command, args, and cwd', async () => {
const mockExtension: GeminiCLIExtension = {
name: 'test-plugin',
path: '/path/to/plugin',
isActive: true,
id: 'test-id',
version: '1.0.0',
} as GeminiCLIExtension;
const config: MCPServerConfig = {
command: 'node',
args: ['${PLUGIN_ROOT}/index.js'],
cwd: '${PLUGIN_ROOT}/src',
extension: mockExtension,
};
await createTransport(
'test-server',
config,
false,
mockMcpContext as McpContext,
);
expect(SdkClientStdioLib.StdioClientTransport).toHaveBeenCalledWith(
expect.objectContaining({
command: 'node',
args: ['/path/to/plugin/index.js'],
cwd: '/path/to/plugin/src',
env: expect.objectContaining({
PLUGIN_ROOT: '/path/to/plugin',
}),
}),
);
});
it('should expand ${PLUGIN_ROOT} in env values', async () => {
const mockExtension: GeminiCLIExtension = {
name: 'test-plugin',
path: '/path/to/plugin',
isActive: true,
id: 'test-id',
version: '1.0.0',
} as GeminiCLIExtension;
const config: MCPServerConfig = {
command: 'node',
args: [],
env: {
PLUGIN_PATH: '${PLUGIN_ROOT}/lib',
},
extension: mockExtension,
};
await createTransport(
'test-server',
config,
false,
mockMcpContext as McpContext,
);
expect(SdkClientStdioLib.StdioClientTransport).toHaveBeenCalledWith(
expect.objectContaining({
env: expect.objectContaining({
PLUGIN_ROOT: '/path/to/plugin',
PLUGIN_PATH: '/path/to/plugin/lib',
}),
}),
);
});
});
@@ -0,0 +1,138 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { McpClientManager } from './mcp-client-manager.js';
import type {
Config,
GeminiCLIExtension,
MCPServerConfig,
} from '../config/config.js';
interface McpClientManagerInternals {
maybeDiscoverMcpServer(name: string, config: MCPServerConfig): Promise<void>;
}
describe('MCP Plugin Namespacing', () => {
let mcpClientManager: McpClientManager;
let mockConfig: Config;
beforeEach(() => {
vi.clearAllMocks();
mockConfig = {
isTrustedFolder: () => true,
getMcpServers: () => ({}),
getMcpServerCommand: () => undefined,
getMcpEnablementCallbacks: () => undefined,
getAllowedMcpServers: () => [],
getBlockedMcpServers: () => [],
getDebugMode: () => false,
getWorkspaceContext: () => ({}),
} as unknown as Config;
mcpClientManager = new McpClientManager('1.0.0', mockConfig);
});
it('should use pluginName:mcpServerName for single MCP server in Open Plugin', async () => {
const extension: GeminiCLIExtension = {
name: 'my-plugin',
manifestType: 'open-plugin',
mcpServers: {
'original-name': {
command: 'node',
args: ['index.js'],
},
},
id: 'test-id',
isActive: true,
} as unknown as GeminiCLIExtension;
const maybeDiscoverSpy = vi
.spyOn(
mcpClientManager as unknown as McpClientManagerInternals,
'maybeDiscoverMcpServer',
)
.mockResolvedValue(undefined);
await mcpClientManager.startExtension(extension);
expect(maybeDiscoverSpy).toHaveBeenCalledWith(
'my-plugin:original-name',
expect.objectContaining({
command: 'node',
extension,
}),
);
});
it('should use pluginName:mcpServerName for multiple MCP servers in Open Plugin', async () => {
const extension: GeminiCLIExtension = {
name: 'multi-plugin',
manifestType: 'open-plugin',
mcpServers: {
s1: { command: 'node', args: ['s1.js'] },
s2: { command: 'node', args: ['s2.js'] },
},
id: 'test-id',
isActive: true,
} as unknown as GeminiCLIExtension;
const maybeDiscoverSpy = vi
.spyOn(
mcpClientManager as unknown as McpClientManagerInternals,
'maybeDiscoverMcpServer',
)
.mockResolvedValue(undefined);
await mcpClientManager.startExtension(extension);
expect(maybeDiscoverSpy).toHaveBeenCalledWith(
'multi-plugin:s1',
expect.objectContaining({
command: 'node',
extension,
}),
);
expect(maybeDiscoverSpy).toHaveBeenCalledWith(
'multi-plugin:s2',
expect.objectContaining({
command: 'node',
extension,
}),
);
});
it('should NOT use plugin name for Gemini extensions', async () => {
const extension: GeminiCLIExtension = {
name: 'gemini-extension',
manifestType: 'gemini',
mcpServers: {
'original-name': {
command: 'node',
args: ['index.js'],
},
},
id: 'test-id',
isActive: true,
} as unknown as GeminiCLIExtension;
const maybeDiscoverSpy = vi
.spyOn(
mcpClientManager as unknown as McpClientManagerInternals,
'maybeDiscoverMcpServer',
)
.mockResolvedValue(undefined);
await mcpClientManager.startExtension(extension);
expect(maybeDiscoverSpy).toHaveBeenCalledWith(
'original-name',
expect.objectContaining({
command: 'node',
extension,
}),
);
});
});
@@ -0,0 +1,60 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import {
isMcpToolName,
parseMcpToolName,
generateValidName,
formatMcpToolName,
} from './mcp-tool.js';
describe('MCP Plugin Naming', () => {
describe('isMcpToolName', () => {
it('should identify standard MCP tool names', () => {
expect(isMcpToolName('mcp_server_tool')).toBe(true);
});
});
describe('parseMcpToolName', () => {
it('should parse standard MCP tool names', () => {
const result = parseMcpToolName('mcp_server_tool');
expect(result.serverName).toBe('server');
expect(result.toolName).toBe('tool');
});
it('should parse MCP names with colons in the server name', () => {
// This is the format for Open Plugins: mcp_plugin:server_tool
const result = parseMcpToolName('mcp_demo-plugin:demo-server_demo_tool');
expect(result.serverName).toBe('demo-plugin:demo-server');
expect(result.toolName).toBe('demo_tool');
});
});
describe('generateValidName', () => {
it('should add mcp_ prefix to standard names', () => {
expect(generateValidName('server_tool')).toBe('mcp_server_tool');
});
it('should include colons if they are in the input', () => {
expect(generateValidName('demo-plugin:demo-server_demo_tool')).toBe(
'mcp_demo-plugin:demo-server_demo_tool',
);
});
});
describe('formatMcpToolName', () => {
it('should format standard MCP tool names', () => {
expect(formatMcpToolName('server', 'tool')).toBe('mcp_server_tool');
});
it('should format Open Plugin namespaced tool names using underscores for the tool portion', () => {
expect(formatMcpToolName('plugin:server', 'tool')).toBe(
'mcp_plugin:server_tool',
);
});
});
});
+2 -2
View File
@@ -2673,8 +2673,8 @@
"enableAgents": {
"title": "Enable Agents",
"description": "Enable local and remote subagents.",
"markdownDescription": "Enable local and remote subagents.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"markdownDescription": "Enable local and remote subagents.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"worktrees": {