Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a3741d2aa | |||
| f7413564ac | |||
| 42344c2666 | |||
| c64c08c774 | |||
| 258490b19c | |||
| afb8727bcc | |||
| ce8315df63 | |||
| 77a6c7e577 | |||
| 2da2f28b20 | |||
| 42a673a52c | |||
| 153f2630b9 | |||
| d5b78dbeea | |||
| 139ef0d5bd | |||
| b2d6dc4e32 | |||
| ac95282758 | |||
| efeb9f7e7b | |||
| 5a65610fa6 | |||
| 447a854ad9 | |||
| b58d79c517 |
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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.');
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -665,7 +665,7 @@ export const renderWithProviders = async (
|
||||
);
|
||||
}
|
||||
|
||||
const mainAreaWidth = terminalWidth;
|
||||
const mainAreaWidth = providedUiState?.mainAreaWidth ?? terminalWidth;
|
||||
|
||||
const finalUiState = {
|
||||
...baseState,
|
||||
|
||||
@@ -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... => 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">) => </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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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">"Line 37"</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">"Line 38"</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">"Line 39"</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">"Line 40"</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">"Line 41"</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">"Line 42"</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">"Line 43"</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">"Line 44"</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">"Line 45"</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">"Line 46"</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">"Line 47"</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">"Line 48"</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">"Line 49"</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">"Line 50"</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: 'echo'?</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">"Line 25"</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">"Line 26"</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">"Line 27"</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">"Line 28"</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">"Line 29"</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">"Line 30"</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">"Line 31"</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">"Line 32"</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">"Line 33"</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">"Line 34"</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">"Line 35"</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">"Line 36"</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">"Line 37"</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">"Line 38"</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">"Line 39"</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">"Line 40"</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">"Line 41"</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">"Line 42"</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">"Line 43"</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">"Line 44"</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">"Line 45"</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">"Line 46"</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">"Line 47"</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">"Line 48"</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">"Line 49"</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">"Line 50"</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: 'echo'?</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">"Line 19"</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">"Line 20"</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">"Line 21"</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">"Line 22"</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">"Line 23"</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">"Line 24"</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">"Line 25"</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">"Line 26"</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">"Line 27"</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">"Line 28"</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">"Line 29"</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">"Line 30"</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">"Line 31"</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">"Line 32"</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">"Line 33"</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">"Line 34"</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">"Line 35"</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">"Line 36"</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">"Line 37"</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">"Line 38"</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">"Line 39"</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">"Line 40"</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">"Line 41"</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">"Line 42"</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">"Line 43"</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">"Line 44"</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">"Line 45"</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">"Line 46"</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">"Line 47"</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">"Line 48"</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">"Line 49"</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">"Line 50"</text>
|
||||
<text x="0" y="563" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Allow execution of: 'echo'?</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 |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ export interface BaseAgentDefinition<
|
||||
metadata?: {
|
||||
hash?: string;
|
||||
filePath?: string;
|
||||
pluginRoot?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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": {
|
||||
|
||||