Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b1e649c26 | |||
| b6d9970af6 | |||
| 201c74c038 | |||
| b7d2aea63b | |||
| 071076aa5c | |||
| 425d64a96f | |||
| c63350cfd6 | |||
| 55a5ed6545 | |||
| 7e0ac00efc | |||
| bbd8483e34 | |||
| 29ab3bbfd7 | |||
| 0f086869d5 | |||
| c711a38f16 | |||
| 975b7dc163 | |||
| c1af5ab99d | |||
| 310db3b823 | |||
| 6222f150fa | |||
| 514bf61903 | |||
| 38c706dbb9 | |||
| 5e18b14c10 | |||
| 8e2629759d | |||
| 146442f2a2 | |||
| a3a3e66922 | |||
| d4555a473e | |||
| cdac58fe82 | |||
| 4a95ab62d4 | |||
| 0223181cf4 |
@@ -1,66 +0,0 @@
|
||||
---
|
||||
name: ci
|
||||
description:
|
||||
A specialized skill for Gemini CLI that provides high-performance, fail-fast
|
||||
monitoring of GitHub Actions workflows and automated local verification of CI
|
||||
failures. It handles run discovery automatically—simply provide the branch name.
|
||||
---
|
||||
|
||||
# CI Replicate & Status
|
||||
|
||||
This skill enables the agent to efficiently monitor GitHub Actions, triage
|
||||
failures, and bridge remote CI errors to local development. It defaults to
|
||||
**automatic replication** of failures to streamline the fix cycle.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **Automatic Replication**: Automatically monitors CI and immediately executes
|
||||
suggested test or lint commands locally upon failure.
|
||||
- **Real-time Monitoring**: Aggregated status line for all concurrent workflows
|
||||
on the current branch.
|
||||
- **Fail-Fast Triage**: Immediately stops on the first job failure to provide a
|
||||
structured report.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. CI Replicate (`replicate`) - DEFAULT
|
||||
Use this as the primary path to monitor CI and **automatically** replicate
|
||||
failures locally for immediate triage and fixing.
|
||||
- **Behavior**: When this workflow is triggered, the agent will monitor the CI
|
||||
and **immediately and automatically execute** all suggested test or lint
|
||||
commands (marked with 🚀) as soon as a failure is detected.
|
||||
- **Tool**: `node .gemini/skills/ci/scripts/ci.mjs [branch]`
|
||||
- **Discovery**: The script **automatically** finds the latest active or recent
|
||||
run for the branch. Do NOT manually search for run IDs.
|
||||
- **Goal**: Reproduce the failure locally without manual intervention, then
|
||||
proceed to analyze and fix the code.
|
||||
|
||||
### 1. CI Status (`status`)
|
||||
Use this when you have pushed changes and need to monitor the CI and reproduce
|
||||
any failures locally.
|
||||
- **Tool**: `node .gemini/skills/ci/scripts/ci.mjs [branch] [run_id]`
|
||||
- **Discovery**: The script **automatically** finds the latest active or recent
|
||||
run for the branch. You should NOT manually search for \`run_id\` using \`gh run list\`
|
||||
unless a specific historical run is requested. Simply provide the branch name.
|
||||
- **Step 1 (Monitor)**: Execute the tool with the branch name.
|
||||
- **Step 2 (Extract)**: Extract suggested \`npm test\` or \`npm run lint\` commands
|
||||
from the output (marked with 🚀).
|
||||
- **Step 3 (Reproduce)**: Execute those commands locally to confirm the failure.
|
||||
- **Behavior**: It will poll every 15 seconds. If it detects a failure, it will
|
||||
exit with a structured report and provide the exact commands to run locally.
|
||||
|
||||
## Failure Categories & Actions
|
||||
|
||||
- **Test Failures**: Agent should run the specific `npm test -w <pkg> -- <path>`
|
||||
command suggested.
|
||||
- **Lint Errors**: Agent should run `npm run lint:all` or the specific package
|
||||
lint command.
|
||||
- **Build Errors**: Agent should check `tsc` output or build logs to resolve
|
||||
compilation issues.
|
||||
- **Job Errors**: Investigate `gh run view --job <job_id> --log` for
|
||||
infrastructure or setup failures.
|
||||
|
||||
## Noise Filtering
|
||||
The underlying scripts automatically filter noise (Git logs, NPM warnings, stack
|
||||
trace overhead). The agent should focus on the "Structured Failure Report"
|
||||
provided by the tool.
|
||||
@@ -1,281 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const BRANCH =
|
||||
process.argv[2] || execSync('git branch --show-current').toString().trim();
|
||||
const RUN_ID_OVERRIDE = process.argv[3];
|
||||
|
||||
let REPO;
|
||||
try {
|
||||
const remoteUrl = execSync('git remote get-url origin').toString().trim();
|
||||
REPO = remoteUrl
|
||||
.replace(/.*github\.com[\/:]/, '')
|
||||
.replace(/\.git$/, '')
|
||||
.trim();
|
||||
} catch (e) {
|
||||
REPO = 'google-gemini/gemini-cli';
|
||||
}
|
||||
|
||||
const FAILED_FILES = new Set();
|
||||
|
||||
function runGh(args) {
|
||||
try {
|
||||
return execSync(`gh ${args}`, {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).toString();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchFailuresViaApi(jobId) {
|
||||
try {
|
||||
const cmd = `gh api repos/${REPO}/actions/jobs/${jobId}/logs | grep -iE " FAIL |❌|ERROR|Lint failed|Build failed|Exception|failed with exit code"`;
|
||||
return execSync(cmd, {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
}).toString();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isNoise(line) {
|
||||
const lower = line.toLowerCase();
|
||||
return (
|
||||
lower.includes('* [new branch]') ||
|
||||
lower.includes('npm warn') ||
|
||||
lower.includes('fetching updates') ||
|
||||
lower.includes('node:internal/errors') ||
|
||||
lower.includes('at ') || // Stack traces
|
||||
lower.includes('checkexecsyncerror') ||
|
||||
lower.includes('node_modules')
|
||||
);
|
||||
}
|
||||
|
||||
function extractTestFile(failureText) {
|
||||
const cleanLine = failureText
|
||||
.replace(/[|#\[\]()]/g, ' ')
|
||||
.replace(/<[^>]*>/g, ' ')
|
||||
.trim();
|
||||
const fileMatch = cleanLine.match(/([\w\/._-]+\.test\.[jt]sx?)/);
|
||||
if (fileMatch) return fileMatch[1];
|
||||
return null;
|
||||
}
|
||||
|
||||
function generateTestCommand(failedFilesMap) {
|
||||
const workspaceToFiles = new Map();
|
||||
for (const [file, info] of failedFilesMap.entries()) {
|
||||
if (
|
||||
['Job Error', 'Unknown File', 'Build Error', 'Lint Error'].includes(file)
|
||||
)
|
||||
continue;
|
||||
let workspace = '@google/gemini-cli';
|
||||
let relPath = file;
|
||||
if (file.startsWith('packages/core/')) {
|
||||
workspace = '@google/gemini-cli-core';
|
||||
relPath = file.replace('packages/core/', '');
|
||||
} else if (file.startsWith('packages/cli/')) {
|
||||
workspace = '@google/gemini-cli';
|
||||
relPath = file.replace('packages/cli/', '');
|
||||
}
|
||||
relPath = relPath.replace(/^.*packages\/[^\/]+\//, '');
|
||||
if (!workspaceToFiles.has(workspace))
|
||||
workspaceToFiles.set(workspace, new Set());
|
||||
workspaceToFiles.get(workspace).add(relPath);
|
||||
}
|
||||
const commands = [];
|
||||
for (const [workspace, files] of workspaceToFiles.entries()) {
|
||||
commands.push(`npm test -w ${workspace} -- ${Array.from(files).join(' ')}`);
|
||||
}
|
||||
return commands.join(' && ');
|
||||
}
|
||||
|
||||
async function monitor() {
|
||||
let targetRunIds = [];
|
||||
if (RUN_ID_OVERRIDE) {
|
||||
targetRunIds = [RUN_ID_OVERRIDE];
|
||||
} else {
|
||||
// 1. Get runs directly associated with the branch
|
||||
const runListOutput = runGh(
|
||||
`run list --branch "${BRANCH}" --limit 10 --json databaseId,status,workflowName,createdAt`,
|
||||
);
|
||||
if (runListOutput) {
|
||||
const runs = JSON.parse(runListOutput);
|
||||
const activeRuns = runs.filter((r) => r.status !== 'completed');
|
||||
if (activeRuns.length > 0) {
|
||||
targetRunIds = activeRuns.map((r) => r.databaseId);
|
||||
} else if (runs.length > 0) {
|
||||
const latestTime = new Date(runs[0].createdAt).getTime();
|
||||
targetRunIds = runs
|
||||
.filter((r) => latestTime - new Date(r.createdAt).getTime() < 60000)
|
||||
.map((r) => r.databaseId);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Get runs associated with commit statuses (handles chained/indirect runs)
|
||||
try {
|
||||
const headSha = execSync(`git rev-parse "${BRANCH}"`).toString().trim();
|
||||
const statusOutput = runGh(
|
||||
`api repos/${REPO}/commits/${headSha}/status -q '.statuses[] | select(.target_url | contains("actions/runs/")) | .target_url'`,
|
||||
);
|
||||
if (statusOutput) {
|
||||
const statusRunIds = statusOutput
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((url) => {
|
||||
const match = url.match(/actions\/runs\/(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
for (const runId of statusRunIds) {
|
||||
if (!targetRunIds.includes(runId)) {
|
||||
targetRunIds.push(runId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore if branch/SHA not found or API fails
|
||||
}
|
||||
|
||||
if (targetRunIds.length > 0) {
|
||||
const runNames = [];
|
||||
for (const runId of targetRunIds) {
|
||||
const runInfo = runGh(`run view "${runId}" --json workflowName`);
|
||||
if (runInfo) {
|
||||
runNames.push(JSON.parse(runInfo).workflowName);
|
||||
}
|
||||
}
|
||||
console.log(`Monitoring workflows: ${[...new Set(runNames)].join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetRunIds.length === 0) {
|
||||
console.log(`No runs found for branch ${BRANCH}.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
let allPassed = 0,
|
||||
allFailed = 0,
|
||||
allRunning = 0,
|
||||
allQueued = 0,
|
||||
totalJobs = 0;
|
||||
let anyRunInProgress = false;
|
||||
const fileToTests = new Map();
|
||||
let failuresFoundInLoop = false;
|
||||
|
||||
for (const runId of targetRunIds) {
|
||||
const runOutput = runGh(
|
||||
`run view "${runId}" --json databaseId,status,conclusion,workflowName`,
|
||||
);
|
||||
if (!runOutput) continue;
|
||||
const run = JSON.parse(runOutput);
|
||||
if (run.status !== 'completed') anyRunInProgress = true;
|
||||
|
||||
const jobsOutput = runGh(`run view "${runId}" --json jobs`);
|
||||
if (jobsOutput) {
|
||||
const { jobs } = JSON.parse(jobsOutput);
|
||||
totalJobs += jobs.length;
|
||||
const failedJobs = jobs.filter((j) => j.conclusion === 'failure');
|
||||
if (failedJobs.length > 0) {
|
||||
failuresFoundInLoop = true;
|
||||
for (const job of failedJobs) {
|
||||
const failures = fetchFailuresViaApi(job.databaseId);
|
||||
if (failures.trim()) {
|
||||
failures.split('\n').forEach((line) => {
|
||||
if (!line.trim() || isNoise(line)) return;
|
||||
const file = extractTestFile(line);
|
||||
const filePath =
|
||||
file ||
|
||||
(line.toLowerCase().includes('lint')
|
||||
? 'Lint Error'
|
||||
: line.toLowerCase().includes('build')
|
||||
? 'Build Error'
|
||||
: 'Unknown File');
|
||||
let testName = line;
|
||||
if (line.includes(' > ')) {
|
||||
testName = line.split(' > ').slice(1).join(' > ').trim();
|
||||
}
|
||||
if (!fileToTests.has(filePath))
|
||||
fileToTests.set(filePath, new Set());
|
||||
fileToTests.get(filePath).add(testName);
|
||||
});
|
||||
} else {
|
||||
const step =
|
||||
job.steps?.find((s) => s.conclusion === 'failure')?.name ||
|
||||
'unknown';
|
||||
const category = step.toLowerCase().includes('lint')
|
||||
? 'Lint Error'
|
||||
: step.toLowerCase().includes('build')
|
||||
? 'Build Error'
|
||||
: 'Job Error';
|
||||
if (!fileToTests.has(category))
|
||||
fileToTests.set(category, new Set());
|
||||
fileToTests
|
||||
.get(category)
|
||||
.add(`${job.name}: Failed at step "${step}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const job of jobs) {
|
||||
if (job.status === 'in_progress') allRunning++;
|
||||
else if (job.status === 'queued') allQueued++;
|
||||
else if (job.conclusion === 'success') allPassed++;
|
||||
else if (job.conclusion === 'failure') allFailed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failuresFoundInLoop) {
|
||||
console.log(
|
||||
`\n\n❌ Failures detected across ${allFailed} job(s). Stopping monitor...`,
|
||||
);
|
||||
console.log('\n--- Structured Failure Report (Noise Filtered) ---');
|
||||
for (const [file, tests] of fileToTests.entries()) {
|
||||
console.log(`\nCategory/File: ${file}`);
|
||||
// Limit output per file if it's too large
|
||||
const testsArr = Array.from(tests).map((t) =>
|
||||
t.length > 500 ? t.substring(0, 500) + '... [TRUNCATED]' : t,
|
||||
);
|
||||
testsArr.slice(0, 10).forEach((t) => console.log(` - ${t}`));
|
||||
if (testsArr.length > 10)
|
||||
console.log(` ... and ${testsArr.length - 10} more`);
|
||||
}
|
||||
const testCmd = generateTestCommand(fileToTests);
|
||||
if (testCmd) {
|
||||
console.log('\n🚀 Run this to verify fixes:');
|
||||
console.log(testCmd);
|
||||
} else if (
|
||||
Array.from(fileToTests.keys()).some((k) => k.includes('Lint'))
|
||||
) {
|
||||
console.log('\n🚀 Run this to verify lint fixes:\nnpm run lint:all');
|
||||
}
|
||||
console.log('---------------------------------');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const completed = allPassed + allFailed;
|
||||
process.stdout.write(
|
||||
`\r⏳ Monitoring ${targetRunIds.length} runs... ${completed}/${totalJobs} jobs (${allPassed} passed, ${allFailed} failed, ${allRunning} running, ${allQueued} queued) `,
|
||||
);
|
||||
if (!anyRunInProgress) {
|
||||
console.log('\n✅ All workflows passed!');
|
||||
process.exit(0);
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 15000));
|
||||
}
|
||||
}
|
||||
|
||||
monitor().catch((err) => {
|
||||
console.error('\nMonitor error:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: code-reviewer
|
||||
description:
|
||||
Use this skill to review code. It supports both local changes (staged or working tree)
|
||||
and remote Pull Requests (by ID or URL). It focuses on correctness, maintainability,
|
||||
and adherence to project standards.
|
||||
---
|
||||
|
||||
# Code Reviewer
|
||||
|
||||
This skill guides the agent in conducting professional and thorough code reviews for both local development and remote Pull Requests.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Determine Review Target
|
||||
* **Remote PR**: If the user provides a PR number or URL (e.g., "Review PR #123"), target that remote PR.
|
||||
* **Local Changes**: If no specific PR is mentioned, or if the user asks to "review my changes", target the current local file system states (staged and unstaged changes).
|
||||
|
||||
### 2. Preparation
|
||||
|
||||
#### For Remote PRs:
|
||||
1. **Checkout**: Use the GitHub CLI to checkout the PR.
|
||||
```bash
|
||||
gh pr checkout <PR_NUMBER>
|
||||
```
|
||||
2. **Preflight**: Execute the project's standard verification suite to catch automated failures early.
|
||||
```bash
|
||||
npm run preflight
|
||||
```
|
||||
3. **Context**: Read the PR description and any existing comments to understand the goal and history.
|
||||
|
||||
#### For Local Changes:
|
||||
1. **Identify Changes**:
|
||||
* Check status: `git status`
|
||||
* Read diffs: `git diff` (working tree) and/or `git diff --staged` (staged).
|
||||
2. **Preflight (Optional)**: If the changes are substantial, ask the user if they want to run `npm run preflight` before reviewing.
|
||||
|
||||
### 3. In-Depth Analysis
|
||||
Analyze the code changes based on the following pillars:
|
||||
|
||||
* **Correctness**: Does the code achieve its stated purpose without bugs or logical errors?
|
||||
* **Maintainability**: Is the code clean, well-structured, and easy to understand and modify in the future? Consider factors like code clarity, modularity, and adherence to established design patterns.
|
||||
* **Readability**: Is the code well-commented (where necessary) and consistently formatted according to our project's coding style guidelines?
|
||||
* **Efficiency**: Are there any obvious performance bottlenecks or resource inefficiencies introduced by the changes?
|
||||
* **Security**: Are there any potential security vulnerabilities or insecure coding practices?
|
||||
* **Edge Cases and Error Handling**: Does the code appropriately handle edge cases and potential errors?
|
||||
* **Testability**: Is the new or modified code adequately covered by tests (even if preflight checks pass)? Suggest additional test cases that would improve coverage or robustness.
|
||||
|
||||
### 4. Provide Feedback
|
||||
|
||||
#### Structure
|
||||
* **Summary**: A high-level overview of the review.
|
||||
* **Findings**:
|
||||
* **Critical**: Bugs, security issues, or breaking changes.
|
||||
* **Improvements**: Suggestions for better code quality or performance.
|
||||
* **Nitpicks**: Formatting or minor style issues (optional).
|
||||
* **Conclusion**: Clear recommendation (Approved / Request Changes).
|
||||
|
||||
#### Tone
|
||||
* Be constructive, professional, and friendly.
|
||||
* Explain *why* a change is requested.
|
||||
* For approvals, acknowledge the specific value of the contribution.
|
||||
|
||||
### 5. Cleanup (Remote PRs only)
|
||||
* After the review, ask the user if they want to switch back to the default branch (e.g., `main` or `master`).
|
||||
@@ -158,12 +158,6 @@ jobs:
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Install system dependencies'
|
||||
run: |
|
||||
sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap
|
||||
# Ubuntu 24.04+ requires this to allow bwrap to function in CI
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
|
||||
- name: 'Install dependencies for testing'
|
||||
run: 'npm ci'
|
||||
|
||||
|
||||
@@ -323,8 +323,8 @@ fi
|
||||
|
||||
#### Formatting
|
||||
|
||||
To separately format the code in this project, run the following command from
|
||||
the root directory:
|
||||
To separately format the code in this project by running the following command
|
||||
from the root directory:
|
||||
|
||||
```bash
|
||||
npm run format
|
||||
|
||||
@@ -92,7 +92,7 @@ To set up runsc:
|
||||
2. Configure the Docker daemon to use the runsc runtime.
|
||||
3. Verify the installation.
|
||||
|
||||
### 5. LXC/LXD (Linux only, experimental)
|
||||
### 4. LXC/LXD (Linux only, experimental)
|
||||
|
||||
Full-system container sandboxing using LXC/LXD. Unlike Docker/Podman, LXC
|
||||
containers run a complete Linux system with `systemd`, `snapd`, and other system
|
||||
|
||||
@@ -30,7 +30,7 @@ they appear in the UI.
|
||||
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. YOLO mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo). | `"default"` |
|
||||
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
|
||||
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
|
||||
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode. | `undefined` |
|
||||
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. | `undefined` |
|
||||
| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` |
|
||||
| Retry Fetch Errors | `general.retryFetchErrors` | Retry on "exception TypeError: fetch failed sending request" errors. | `true` |
|
||||
| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` |
|
||||
|
||||
@@ -143,8 +143,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **`general.plan.directory`** (string):
|
||||
- **Description:** The directory where planning artifacts are stored. If not
|
||||
specified, defaults to the system temporary directory. A custom directory
|
||||
requires a policy to allow write access in Plan Mode.
|
||||
specified, defaults to the system temporary directory.
|
||||
- **Default:** `undefined`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
@@ -646,6 +645,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"model": "gemini-3-flash-preview"
|
||||
}
|
||||
},
|
||||
"chat-compression-3.1-flash-lite": {
|
||||
"modelConfig": {
|
||||
"model": "gemini-3.1-flash-lite-preview"
|
||||
}
|
||||
},
|
||||
"chat-compression-2.5-pro": {
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-pro"
|
||||
@@ -850,12 +854,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"hasAccessToPreview": false
|
||||
},
|
||||
"target": "gemini-2.5-pro"
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"useCustomTools": true
|
||||
},
|
||||
"target": "gemini-3.1-pro-preview-customtools"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -980,6 +978,17 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"auto-gemini-2.5": {
|
||||
"default": "gemini-2.5-pro"
|
||||
},
|
||||
"gemini-3.1-flash-lite-preview": {
|
||||
"default": "gemini-3.1-flash-lite-preview",
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"useGemini3_1FlashLite": false
|
||||
},
|
||||
"target": "gemini-2.5-flash-lite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"flash": {
|
||||
"default": "gemini-3-flash-preview",
|
||||
"contexts": [
|
||||
@@ -992,7 +1001,15 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
]
|
||||
},
|
||||
"flash-lite": {
|
||||
"default": "gemini-2.5-flash-lite"
|
||||
"default": "gemini-2.5-flash-lite",
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"useGemini3_1FlashLite": true
|
||||
},
|
||||
"target": "gemini-3.1-flash-lite-preview"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1222,11 +1239,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Disable user input on browser window during automation.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`agents.browser.maxActionsPerTask`** (number):
|
||||
- **Description:** The maximum number of tool calls allowed per browser task.
|
||||
Enforcement is hard: the agent will be terminated when the limit is reached.
|
||||
- **Default:** `100`
|
||||
|
||||
- **`agents.browser.confirmSensitiveActions`** (boolean):
|
||||
- **Description:** Require manual confirmation for sensitive browser actions
|
||||
(e.g., fill_form, evaluate_script).
|
||||
@@ -1588,7 +1600,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **`experimental.jitContext`** (boolean):
|
||||
- **Description:** Enable Just-In-Time (JIT) context loading.
|
||||
- **Default:** `true`
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.useOSC52Paste`** (boolean):
|
||||
@@ -2353,13 +2365,9 @@ can be based on the base sandbox image:
|
||||
```dockerfile
|
||||
FROM gemini-cli-sandbox
|
||||
|
||||
# Add your custom dependencies or configurations here.
|
||||
# Note: The base image runs as the non-root 'node' user.
|
||||
# You must switch to 'root' to install system packages.
|
||||
# Add your custom dependencies or configurations here
|
||||
# For example:
|
||||
# USER root
|
||||
# RUN apt-get update && apt-get install -y some-package
|
||||
# USER node
|
||||
# COPY ./my-config /app/my-config
|
||||
```
|
||||
|
||||
|
||||
@@ -63,62 +63,29 @@ details.
|
||||
|
||||
## Available tools
|
||||
|
||||
The following sections list all available tools, categorized by their primary
|
||||
function. For detailed parameter information, see the linked documentation for
|
||||
each tool.
|
||||
The following table lists all available tools, categorized by their primary
|
||||
function.
|
||||
|
||||
### Execution
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :--------------------------------------- | :-------- | :----------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`run_shell_command`](../tools/shell.md) | `Execute` | Executes arbitrary shell commands. Supports interactive sessions and background processes. Requires manual confirmation. |
|
||||
|
||||
### File System
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :------------------------------------------- | :------- | :---------------------------------------------------------------------------------------------------- |
|
||||
| [`glob`](../tools/file-system.md) | `Search` | Finds files matching specific glob patterns across the workspace. |
|
||||
| [`grep_search`](../tools/file-system.md) | `Search` | Searches for a regular expression pattern within file contents. Legacy alias: `search_file_content`. |
|
||||
| [`list_directory`](../tools/file-system.md) | `Read` | Lists the names of files and subdirectories within a specified path. |
|
||||
| [`read_file`](../tools/file-system.md) | `Read` | Reads the content of a specific file. Supports text, images, audio, and PDF. |
|
||||
| [`read_many_files`](../tools/file-system.md) | `Read` | Reads and concatenates content from multiple files. Often triggered by the `@` symbol in your prompt. |
|
||||
| [`replace`](../tools/file-system.md) | `Edit` | Performs precise text replacement within a file. Requires manual confirmation. |
|
||||
| [`write_file`](../tools/file-system.md) | `Edit` | Creates or overwrites a file with new content. Requires manual confirmation. |
|
||||
|
||||
### Interaction
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :--------------------------------- | :------------ | :------------------------------------------------------------------------------------- |
|
||||
| [`ask_user`](../tools/ask-user.md) | `Communicate` | Requests clarification or missing information via an interactive dialog. |
|
||||
| [`write_todos`](../tools/todos.md) | `Other` | Maintains an internal list of subtasks. The model uses this to track its own progress. |
|
||||
|
||||
### Memory
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :----------------------------------------------- | :------ | :----------------------------------------------------------------------------------- |
|
||||
| [`activate_skill`](../tools/activate-skill.md) | `Other` | Loads specialized procedural expertise from the `.gemini/skills` directory. |
|
||||
| [`get_internal_docs`](../tools/internal-docs.md) | `Think` | Accesses Gemini CLI's own documentation for accurate answers about its capabilities. |
|
||||
| [`save_memory`](../tools/memory.md) | `Think` | Persists specific facts and project details to your `GEMINI.md` file. |
|
||||
|
||||
### Planning
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :---------------------------------------- | :----- | :--------------------------------------------------------------------------------------- |
|
||||
| [`enter_plan_mode`](../tools/planning.md) | `Plan` | Switches the CLI to a safe, read-only "Plan Mode" for researching complex changes. |
|
||||
| [`exit_plan_mode`](../tools/planning.md) | `Plan` | Finalizes a plan, presents it for review, and requests approval to start implementation. |
|
||||
|
||||
### System
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :-------------- | :------ | :----------------------------------------------------------------------------------------------------------------- |
|
||||
| `complete_task` | `Other` | Finalizes a subagent's mission and returns the result to the parent agent. This tool is not available to the user. |
|
||||
|
||||
### Web
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :-------------------------------------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`google_web_search`](../tools/web-search.md) | `Search` | Performs a Google Search to find up-to-date information. |
|
||||
| [`web_fetch`](../tools/web-fetch.md) | `Fetch` | Retrieves and processes content from specific URLs. **Warning:** This tool can access local and private network addresses (e.g., localhost), which may pose a security risk if used with untrusted prompts. |
|
||||
| Category | Tool | Kind | Description |
|
||||
| :---------- | :----------------------------------------------- | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Execution | [`run_shell_command`](../tools/shell.md) | `Execute` | Executes arbitrary shell commands. Supports interactive sessions and background processes. Requires manual confirmation.<br><br>**Parameters:** `command`, `description`, `dir_path`, `is_background` |
|
||||
| File System | [`glob`](../tools/file-system.md) | `Search` | Finds files matching specific glob patterns across the workspace.<br><br>**Parameters:** `pattern`, `dir_path`, `case_sensitive`, `respect_git_ignore`, `respect_gemini_ignore` |
|
||||
| File System | [`grep_search`](../tools/file-system.md) | `Search` | Searches for a regular expression pattern within file contents. Legacy alias: `search_file_content`.<br><br>**Parameters:** `pattern`, `dir_path`, `include`, `exclude_pattern`, `names_only`, `max_matches_per_file`, `total_max_matches` |
|
||||
| File System | [`list_directory`](../tools/file-system.md) | `Read` | Lists the names of files and subdirectories within a specified path.<br><br>**Parameters:** `dir_path`, `ignore`, `file_filtering_options` |
|
||||
| File System | [`read_file`](../tools/file-system.md) | `Read` | Reads the content of a specific file. Supports text, images, audio, and PDF.<br><br>**Parameters:** `file_path`, `start_line`, `end_line` |
|
||||
| File System | [`read_many_files`](../tools/file-system.md) | `Read` | Reads and concatenates content from multiple files. Often triggered by the `@` symbol in your prompt.<br><br>**Parameters:** `include`, `exclude`, `recursive`, `useDefaultExcludes`, `file_filtering_options` |
|
||||
| File System | [`replace`](../tools/file-system.md) | `Edit` | Performs precise text replacement within a file. Requires manual confirmation.<br><br>**Parameters:** `file_path`, `instruction`, `old_string`, `new_string`, `allow_multiple` |
|
||||
| File System | [`write_file`](../tools/file-system.md) | `Edit` | Creates or overwrites a file with new content. Requires manual confirmation.<br><br>**Parameters:** `file_path`, `content` |
|
||||
| Interaction | [`ask_user`](../tools/ask-user.md) | `Communicate` | Requests clarification or missing information via an interactive dialog.<br><br>**Parameters:** `questions` |
|
||||
| Interaction | [`write_todos`](../tools/todos.md) | `Other` | Maintains an internal list of subtasks. The model uses this to track its own progress and display it to you.<br><br>**Parameters:** `todos` |
|
||||
| Memory | [`activate_skill`](../tools/activate-skill.md) | `Other` | Loads specialized procedural expertise for specific tasks from the `.gemini/skills` directory.<br><br>**Parameters:** `name` |
|
||||
| Memory | [`get_internal_docs`](../tools/internal-docs.md) | `Think` | Accesses Gemini CLI's own documentation to provide more accurate answers about its capabilities.<br><br>**Parameters:** `path` |
|
||||
| Memory | [`save_memory`](../tools/memory.md) | `Think` | Persists specific facts and project details to your `GEMINI.md` file to retain context.<br><br>**Parameters:** `fact` |
|
||||
| Planning | [`enter_plan_mode`](../tools/planning.md) | `Plan` | Switches the CLI to a safe, read-only "Plan Mode" for researching complex changes.<br><br>**Parameters:** `reason` |
|
||||
| Planning | [`exit_plan_mode`](../tools/planning.md) | `Plan` | Finalizes a plan, presents it for review, and requests approval to start implementation.<br><br>**Parameters:** `plan` |
|
||||
| System | `complete_task` | `Other` | Finalizes a subagent's mission and returns the result to the parent agent. This tool is not available to the user.<br><br>**Parameters:** `result` |
|
||||
| Web | [`google_web_search`](../tools/web-search.md) | `Search` | Performs a Google Search to find up-to-date information.<br><br>**Parameters:** `query` |
|
||||
| Web | [`web_fetch`](../tools/web-fetch.md) | `Fetch` | Retrieves and processes content from specific URLs. **Warning:** This tool can access local and private network addresses (e.g., localhost), which may pose a security risk if used with untrusted prompts.<br><br>**Parameters:** `prompt` |
|
||||
|
||||
## Under the hood
|
||||
|
||||
|
||||
@@ -9,7 +9,27 @@ import path from 'node:path';
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
|
||||
import { evalTest, TEST_AGENTS } from './test-helper.js';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
const DOCS_AGENT_DEFINITION = `---
|
||||
name: docs-agent
|
||||
description: An agent with expertise in updating documentation.
|
||||
tools:
|
||||
- read_file
|
||||
- write_file
|
||||
---
|
||||
You are the docs agent. Update documentation clearly and accurately.
|
||||
`;
|
||||
|
||||
const TEST_AGENT_DEFINITION = `---
|
||||
name: test-agent
|
||||
description: An agent with expertise in writing and updating tests.
|
||||
tools:
|
||||
- read_file
|
||||
- write_file
|
||||
---
|
||||
You are the test agent. Add or update tests.
|
||||
`;
|
||||
|
||||
const INDEX_TS = 'export const add = (a: number, b: number) => a + b;\n';
|
||||
|
||||
@@ -42,12 +62,12 @@ describe('subagent eval test cases', () => {
|
||||
},
|
||||
prompt: 'Please update README.md with a description of this library.',
|
||||
files: {
|
||||
...TEST_AGENTS.DOCS_AGENT.asFile(),
|
||||
'.gemini/agents/docs-agent.md': DOCS_AGENT_DEFINITION,
|
||||
'index.ts': INDEX_TS,
|
||||
'README.md': 'TODO: update the README.\n',
|
||||
},
|
||||
assert: async (rig, _result) => {
|
||||
await rig.expectToolCallSuccess([TEST_AGENTS.DOCS_AGENT.name]);
|
||||
await rig.expectToolCallSuccess(['docs-agent']);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -72,7 +92,7 @@ describe('subagent eval test cases', () => {
|
||||
prompt:
|
||||
'Rename the exported function in index.ts from add to sum and update the file directly.',
|
||||
files: {
|
||||
...TEST_AGENTS.DOCS_AGENT.asFile(),
|
||||
'.gemini/agents/docs-agent.md': DOCS_AGENT_DEFINITION,
|
||||
'index.ts': INDEX_TS,
|
||||
},
|
||||
assert: async (rig, _result) => {
|
||||
@@ -82,11 +102,9 @@ describe('subagent eval test cases', () => {
|
||||
}>;
|
||||
|
||||
expect(updatedIndex).toContain('export const sum =');
|
||||
expect(
|
||||
toolLogs.some(
|
||||
(l) => l.toolRequest.name === TEST_AGENTS.DOCS_AGENT.name,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(toolLogs.some((l) => l.toolRequest.name === 'docs-agent')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
|
||||
false,
|
||||
);
|
||||
@@ -115,7 +133,7 @@ describe('subagent eval test cases', () => {
|
||||
},
|
||||
prompt: 'Please add a small test file that verifies add(1, 2) returns 3.',
|
||||
files: {
|
||||
...TEST_AGENTS.TESTING_AGENT.asFile(),
|
||||
'.gemini/agents/test-agent.md': TEST_AGENT_DEFINITION,
|
||||
'index.ts': INDEX_TS,
|
||||
'package.json': JSON.stringify(
|
||||
{
|
||||
@@ -132,7 +150,7 @@ describe('subagent eval test cases', () => {
|
||||
toolRequest: { name: string };
|
||||
}>;
|
||||
|
||||
await rig.expectToolCallSuccess([TEST_AGENTS.TESTING_AGENT.name]);
|
||||
await rig.expectToolCallSuccess(['test-agent']);
|
||||
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
|
||||
false,
|
||||
);
|
||||
@@ -160,8 +178,8 @@ describe('subagent eval test cases', () => {
|
||||
prompt:
|
||||
'Add a short README description for this library and also add a test file that verifies add(1, 2) returns 3.',
|
||||
files: {
|
||||
...TEST_AGENTS.DOCS_AGENT.asFile(),
|
||||
...TEST_AGENTS.TESTING_AGENT.asFile(),
|
||||
'.gemini/agents/docs-agent.md': DOCS_AGENT_DEFINITION,
|
||||
'.gemini/agents/test-agent.md': TEST_AGENT_DEFINITION,
|
||||
'index.ts': INDEX_TS,
|
||||
'README.md': 'TODO: update the README.\n',
|
||||
'package.json': JSON.stringify(
|
||||
@@ -180,10 +198,7 @@ describe('subagent eval test cases', () => {
|
||||
}>;
|
||||
const readme = readProjectFile(rig, 'README.md');
|
||||
|
||||
await rig.expectToolCallSuccess([
|
||||
TEST_AGENTS.DOCS_AGENT.name,
|
||||
TEST_AGENTS.TESTING_AGENT.name,
|
||||
]);
|
||||
await rig.expectToolCallSuccess(['docs-agent', 'test-agent']);
|
||||
expect(readme).not.toContain('TODO: update the README.');
|
||||
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
|
||||
false,
|
||||
|
||||
@@ -34,20 +34,16 @@ describe('extension install', () => {
|
||||
writeFileSync(testServerPath, extension);
|
||||
try {
|
||||
const result = await rig.runCommand(
|
||||
['--debug', 'extensions', 'install', `${rig.testDir!}`],
|
||||
['extensions', 'install', `${rig.testDir!}`],
|
||||
{ stdin: 'y\n' },
|
||||
);
|
||||
expect(result).toContain('test-extension-install');
|
||||
|
||||
const listResult = await rig.runCommand([
|
||||
'--debug',
|
||||
'extensions',
|
||||
'list',
|
||||
]);
|
||||
const listResult = await rig.runCommand(['extensions', 'list']);
|
||||
expect(listResult).toContain('test-extension-install');
|
||||
writeFileSync(testServerPath, extensionUpdate);
|
||||
const updateResult = await rig.runCommand(
|
||||
['--debug', 'extensions', 'update', `test-extension-install`],
|
||||
['extensions', 'update', `test-extension-install`],
|
||||
{ stdin: 'y\n' },
|
||||
);
|
||||
expect(updateResult).toContain('0.0.2');
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('extension reloading', () => {
|
||||
}
|
||||
|
||||
const result = await rig.runCommand(
|
||||
['--debug', 'extensions', 'install', `${rig.testDir!}`],
|
||||
['extensions', 'install', `${rig.testDir!}`],
|
||||
{ stdin: 'y\n' },
|
||||
);
|
||||
expect(result).toContain('test-extension');
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"ink": "npm:@jrichman/ink@6.4.11",
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"latest-version": "^9.0.0",
|
||||
"node-fetch-native": "^1.6.7",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
@@ -92,46 +92,6 @@
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@alcalzone/ansi-tokenize": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.2.tgz",
|
||||
"integrity": "sha512-mkOh+Wwawzuf5wa30bvc4nA+Qb6DIrGWgBhRR/Pw4T9nsgYait8izvXkNyU78D6Wcu3Z+KUdwCmLCxlWjEotYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"is-fullwidth-code-point": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@alcalzone/ansi-tokenize/node_modules/is-fullwidth-code-point": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
@@ -10089,14 +10049,13 @@
|
||||
},
|
||||
"node_modules/ink": {
|
||||
"name": "@jrichman/ink",
|
||||
"version": "6.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz",
|
||||
"integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==",
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.3.tgz",
|
||||
"integrity": "sha512-0v4S7TbbF2tpQrfqH1btwLgTgH+K0vY2BJbokTE5Lk1KBr4TqZ+Pyo+geSD5F+zytX6G2ajGHBQyHk8yGK4C7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
"ansi-styles": "^6.2.1",
|
||||
"ansi-styles": "^6.2.3",
|
||||
"auto-bind": "^5.0.1",
|
||||
"chalk": "^5.6.0",
|
||||
"cli-boxes": "^3.0.0",
|
||||
@@ -10105,6 +10064,7 @@
|
||||
"code-excerpt": "^4.0.0",
|
||||
"es-toolkit": "^1.39.10",
|
||||
"indent-string": "^5.0.0",
|
||||
"is-fullwidth-code-point": "^5.0.0",
|
||||
"is-in-ci": "^2.0.0",
|
||||
"mnemonist": "^0.40.3",
|
||||
"patch-console": "^2.0.0",
|
||||
@@ -10116,6 +10076,7 @@
|
||||
"type-fest": "^4.27.0",
|
||||
"wrap-ansi": "^9.0.0",
|
||||
"ws": "^8.18.0",
|
||||
"yargs": "^17.7.2",
|
||||
"yoga-layout": "~3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -10173,9 +10134,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ink/node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -10196,6 +10157,21 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ink/node_modules/is-fullwidth-code-point": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ink/node_modules/is-in-ci": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz",
|
||||
@@ -17413,7 +17389,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -17528,7 +17504,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
@@ -17550,7 +17526,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^12.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "npm:@jrichman/ink@6.4.11",
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
@@ -17700,7 +17676,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
@@ -17966,7 +17942,7 @@
|
||||
},
|
||||
"packages/devtools": {
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.16.0"
|
||||
@@ -17981,7 +17957,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -17998,7 +17974,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -18015,7 +17991,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
@@ -68,7 +68,7 @@
|
||||
"pre-commit": "node scripts/pre-commit.js"
|
||||
},
|
||||
"overrides": {
|
||||
"ink": "npm:@jrichman/ink@6.4.11",
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"wrap-ansi": "9.0.2",
|
||||
"cliui": {
|
||||
"wrap-ansi": "7.0.0"
|
||||
@@ -136,7 +136,7 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ink": "npm:@jrichman/ink@6.4.11",
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"latest-version": "^9.0.0",
|
||||
"node-fetch-native": "^1.6.7",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -352,23 +352,37 @@ describe('loadConfig', () => {
|
||||
});
|
||||
|
||||
describe('interactivity', () => {
|
||||
it('should set interactive true when not headless', async () => {
|
||||
it('should always set interactive true', async () => {
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(true);
|
||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||
expect(Config).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
interactive: true,
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(false);
|
||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||
expect(Config).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
interactive: true,
|
||||
enableInteractiveShell: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set interactive false when headless', async () => {
|
||||
it('should set enableInteractiveShell based on headless mode', async () => {
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(false);
|
||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||
expect(Config).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
enableInteractiveShell: true,
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(true);
|
||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||
expect(Config).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
interactive: false,
|
||||
enableInteractiveShell: false,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -125,7 +125,7 @@ export async function loadConfig(
|
||||
trustedFolder: true,
|
||||
extensionLoader,
|
||||
checkpointing,
|
||||
interactive: !isHeadlessMode(),
|
||||
interactive: true,
|
||||
enableInteractiveShell: !isHeadlessMode(),
|
||||
ptyInfo: 'auto',
|
||||
enableAgents: settings.experimental?.enableAgents ?? true,
|
||||
|
||||
@@ -6,19 +6,12 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// --- Fast Path for Version ---
|
||||
// We check for version flags at the very top to avoid loading any heavy dependencies.
|
||||
// process.env.CLI_VERSION is defined during the build process by esbuild.
|
||||
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
||||
console.log(process.env['CLI_VERSION'] || 'unknown');
|
||||
process.exit(0);
|
||||
}
|
||||
import { main } from './src/gemini.js';
|
||||
import { FatalError, writeToStderr } from '@google/gemini-cli-core';
|
||||
import { runExitCleanup } from './src/utils/cleanup.js';
|
||||
|
||||
// --- Global Entry Point ---
|
||||
|
||||
let writeToStderrFn: (message: string) => void = (msg) =>
|
||||
process.stderr.write(msg);
|
||||
|
||||
// Suppress known race condition error in node-pty on Windows
|
||||
// Tracking bug: https://github.com/microsoft/node-pty/issues/827
|
||||
process.on('uncaughtException', (error) => {
|
||||
@@ -35,22 +28,13 @@ process.on('uncaughtException', (error) => {
|
||||
// For other errors, we rely on the default behavior, but since we attached a listener,
|
||||
// we must manually replicate it.
|
||||
if (error instanceof Error) {
|
||||
writeToStderrFn(error.stack + '\n');
|
||||
writeToStderr(error.stack + '\n');
|
||||
} else {
|
||||
writeToStderrFn(String(error) + '\n');
|
||||
writeToStderr(String(error) + '\n');
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const [{ main }, { FatalError, writeToStderr }, { runExitCleanup }] =
|
||||
await Promise.all([
|
||||
import('./src/gemini.js'),
|
||||
import('@google/gemini-cli-core'),
|
||||
import('./src/utils/cleanup.js'),
|
||||
]);
|
||||
|
||||
writeToStderrFn = writeToStderr;
|
||||
|
||||
main().catch(async (error) => {
|
||||
// Set a timeout to force exit if cleanup hangs
|
||||
const cleanupTimeout = setTimeout(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.36.0",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -27,7 +27,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
@@ -49,7 +49,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^12.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "npm:@jrichman/ink@6.4.11",
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
|
||||
@@ -21,13 +21,13 @@ import {
|
||||
AuthType,
|
||||
ToolConfirmationOutcome,
|
||||
StreamEventType,
|
||||
isWithinRoot,
|
||||
ReadManyFilesTool,
|
||||
type GeminiChat,
|
||||
type Config,
|
||||
type MessageBus,
|
||||
LlmRole,
|
||||
type GitService,
|
||||
processSingleFileContent,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
SettingScope,
|
||||
@@ -111,6 +111,7 @@ vi.mock(
|
||||
}),
|
||||
})),
|
||||
logToolCall: vi.fn(),
|
||||
isWithinRoot: vi.fn().mockReturnValue(true),
|
||||
LlmRole: {
|
||||
MAIN: 'main',
|
||||
SUBAGENT: 'subagent',
|
||||
@@ -133,7 +134,6 @@ vi.mock(
|
||||
Cancelled: 'cancelled',
|
||||
AwaitingApproval: 'awaiting_approval',
|
||||
},
|
||||
processSingleFileContent: vi.fn(),
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -177,10 +177,6 @@ describe('GeminiAgent', () => {
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||
validatePathAccess: vi.fn().mockReturnValue(null),
|
||||
getWorkspaceContext: vi.fn().mockReturnValue({
|
||||
addReadOnlyPath: vi.fn(),
|
||||
}),
|
||||
get config() {
|
||||
return this;
|
||||
},
|
||||
@@ -195,7 +191,6 @@ describe('GeminiAgent', () => {
|
||||
mockArgv = {} as unknown as CliArgs;
|
||||
mockConnection = {
|
||||
sessionUpdate: vi.fn(),
|
||||
requestPermission: vi.fn(),
|
||||
} as unknown as Mocked<acp.AgentSideConnection>;
|
||||
|
||||
(loadCliConfig as unknown as Mock).mockResolvedValue(mockConfig);
|
||||
@@ -653,7 +648,6 @@ describe('Session', () => {
|
||||
shouldIgnoreFile: vi.fn().mockReturnValue(false),
|
||||
}),
|
||||
getFileFilteringOptions: vi.fn().mockReturnValue({}),
|
||||
getFileSystemService: vi.fn().mockReturnValue({}),
|
||||
getTargetDir: vi.fn().mockReturnValue('/tmp'),
|
||||
getEnableRecursiveFileSearch: vi.fn().mockReturnValue(false),
|
||||
getDebugMode: vi.fn().mockReturnValue(false),
|
||||
@@ -663,10 +657,6 @@ describe('Session', () => {
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
getGitService: vi.fn().mockResolvedValue({} as GitService),
|
||||
validatePathAccess: vi.fn().mockReturnValue(null),
|
||||
getWorkspaceContext: vi.fn().mockReturnValue({
|
||||
addReadOnlyPath: vi.fn(),
|
||||
}),
|
||||
waitForMcpInit: vi.fn(),
|
||||
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||
get config() {
|
||||
@@ -1366,6 +1356,7 @@ describe('Session', () => {
|
||||
(fs.stat as unknown as Mock).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
});
|
||||
(isWithinRoot as unknown as Mock).mockReturnValue(true);
|
||||
|
||||
const stream = createMockStream([
|
||||
{
|
||||
@@ -1423,6 +1414,7 @@ describe('Session', () => {
|
||||
(fs.stat as unknown as Mock).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
});
|
||||
(isWithinRoot as unknown as Mock).mockReturnValue(true);
|
||||
|
||||
const MockReadManyFilesTool = ReadManyFilesTool as unknown as Mock;
|
||||
MockReadManyFilesTool.mockImplementationOnce(() => ({
|
||||
@@ -1476,172 +1468,6 @@ describe('Session', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle @path validation error and bubble it to user', async () => {
|
||||
mockConfig.getTargetDir.mockReturnValue('/workspace');
|
||||
(path.resolve as unknown as Mock).mockReturnValue('/tmp/disallowed.txt');
|
||||
mockConfig.validatePathAccess.mockReturnValue('Path is outside workspace');
|
||||
|
||||
// Force fs.stat to fail to skip direct reading and triggers the warning
|
||||
(fs.stat as unknown as Mock).mockRejectedValue(new Error('File not found'));
|
||||
|
||||
const stream = createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: { candidates: [] },
|
||||
},
|
||||
]);
|
||||
mockChat.sendMessageStream.mockResolvedValue(stream);
|
||||
|
||||
await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [
|
||||
{
|
||||
type: 'resource_link',
|
||||
uri: 'file://disallowed.txt',
|
||||
mimeType: 'text/plain',
|
||||
name: 'disallowed.txt',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Verify warning sent via sendUpdate
|
||||
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
update: expect.objectContaining({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: expect.objectContaining({
|
||||
text: expect.stringContaining(
|
||||
'Warning: skipping access to `disallowed.txt`. Reason: Path is outside workspace',
|
||||
),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should read absolute file directly if outside workspace', async () => {
|
||||
mockConfig.getTargetDir.mockReturnValue('/workspace');
|
||||
const testFilePath = '/tmp/custom.txt';
|
||||
(path.resolve as unknown as Mock).mockReturnValue(testFilePath);
|
||||
mockConfig.validatePathAccess.mockReturnValue('Path is outside workspace');
|
||||
|
||||
mockConnection.requestPermission.mockResolvedValue({
|
||||
outcome: {
|
||||
outcome: 'selected',
|
||||
optionId: ToolConfirmationOutcome.ProceedOnce,
|
||||
},
|
||||
} as unknown as acp.RequestPermissionResponse);
|
||||
|
||||
const mockStats = {
|
||||
isFile: () => true,
|
||||
isDirectory: () => false,
|
||||
};
|
||||
(fs.stat as unknown as Mock).mockResolvedValue(mockStats);
|
||||
(processSingleFileContent as unknown as Mock).mockResolvedValue({
|
||||
llmContent: 'Absolute File Content',
|
||||
});
|
||||
|
||||
const stream = createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: { candidates: [] },
|
||||
},
|
||||
]);
|
||||
mockChat.sendMessageStream.mockResolvedValue(stream);
|
||||
|
||||
await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [
|
||||
{
|
||||
type: 'resource_link',
|
||||
uri: `file://${testFilePath}`,
|
||||
mimeType: 'text/plain',
|
||||
name: 'custom.txt',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(processSingleFileContent).toHaveBeenCalledWith(
|
||||
testFilePath,
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
// Verify content appended to sendMessageStream parts
|
||||
expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'Absolute File Content',
|
||||
}),
|
||||
]),
|
||||
expect.anything(),
|
||||
expect.any(AbortSignal),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should read escaping relative file directly if outside workspace', async () => {
|
||||
mockConfig.getTargetDir.mockReturnValue('/workspace');
|
||||
const testFilePath = '../../custom.txt';
|
||||
(path.resolve as unknown as Mock).mockReturnValue('/custom.txt');
|
||||
mockConfig.validatePathAccess.mockReturnValue('Path is outside workspace');
|
||||
|
||||
mockConnection.requestPermission.mockResolvedValue({
|
||||
outcome: {
|
||||
outcome: 'selected',
|
||||
optionId: ToolConfirmationOutcome.ProceedOnce,
|
||||
},
|
||||
} as unknown as acp.RequestPermissionResponse);
|
||||
|
||||
const mockStats = {
|
||||
isFile: () => true,
|
||||
isDirectory: () => false,
|
||||
};
|
||||
(fs.stat as unknown as Mock).mockResolvedValue(mockStats);
|
||||
(processSingleFileContent as unknown as Mock).mockResolvedValue({
|
||||
llmContent: 'Escaping Relative File Content',
|
||||
});
|
||||
|
||||
const stream = createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: { candidates: [] },
|
||||
},
|
||||
]);
|
||||
mockChat.sendMessageStream.mockResolvedValue(stream);
|
||||
|
||||
await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [
|
||||
{
|
||||
type: 'resource_link',
|
||||
uri: `file://${testFilePath}`,
|
||||
mimeType: 'text/plain',
|
||||
name: 'custom.txt',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(processSingleFileContent).toHaveBeenCalledWith(
|
||||
'/custom.txt',
|
||||
expect.any(String),
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'Escaping Relative File Content',
|
||||
}),
|
||||
]),
|
||||
expect.anything(),
|
||||
expect.any(AbortSignal),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle cancellation during prompt', async () => {
|
||||
let streamController: ReadableStreamDefaultController<unknown>;
|
||||
const stream = new ReadableStream({
|
||||
@@ -1840,6 +1666,7 @@ describe('Session', () => {
|
||||
(fs.stat as unknown as Mock).mockResolvedValue({
|
||||
isDirectory: () => true,
|
||||
});
|
||||
(isWithinRoot as unknown as Mock).mockReturnValue(true);
|
||||
|
||||
const stream = createMockStream([
|
||||
{
|
||||
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
getDisplayString,
|
||||
processSingleFileContent,
|
||||
type AgentLoopContext,
|
||||
} from '@google/gemini-cli-core';
|
||||
import * as acp from '@agentclientprotocol/sdk';
|
||||
@@ -74,17 +73,6 @@ import { runExitCleanup } from '../utils/cleanup.js';
|
||||
import { SessionSelector } from '../utils/sessionUtils.js';
|
||||
|
||||
import { CommandHandler } from './commandHandler.js';
|
||||
|
||||
const RequestPermissionResponseSchema = z.object({
|
||||
outcome: z.discriminatedUnion('outcome', [
|
||||
z.object({ outcome: z.literal('cancelled') }),
|
||||
z.object({
|
||||
outcome: z.literal('selected'),
|
||||
optionId: z.string(),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
export async function runAcpClient(
|
||||
config: Config,
|
||||
settings: LoadedSettings,
|
||||
@@ -1023,12 +1011,10 @@ export class Session {
|
||||
},
|
||||
};
|
||||
|
||||
const output = RequestPermissionResponseSchema.parse(
|
||||
await this.connection.requestPermission(params),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const output = await this.connection.requestPermission(params);
|
||||
const outcome =
|
||||
output.outcome.outcome === 'cancelled'
|
||||
output.outcome.outcome === CoreToolCallStatus.Cancelled
|
||||
? ToolConfirmationOutcome.Cancel
|
||||
: z
|
||||
.nativeEnum(ToolConfirmationOutcome)
|
||||
@@ -1239,11 +1225,6 @@ export class Session {
|
||||
const pathSpecsToRead: string[] = [];
|
||||
const contentLabelsForDisplay: string[] = [];
|
||||
const ignoredPaths: string[] = [];
|
||||
const directContents: Array<{
|
||||
spec: string;
|
||||
content?: string;
|
||||
part?: Part;
|
||||
}> = [];
|
||||
|
||||
const toolRegistry = this.context.toolRegistry;
|
||||
const readManyFilesTool = new ReadManyFilesTool(
|
||||
@@ -1266,197 +1247,28 @@ export class Session {
|
||||
}
|
||||
let currentPathSpec = pathName;
|
||||
let resolvedSuccessfully = false;
|
||||
let readDirectly = false;
|
||||
try {
|
||||
const absolutePath = path.resolve(
|
||||
this.context.config.getTargetDir(),
|
||||
pathName,
|
||||
);
|
||||
|
||||
let validationError = this.context.config.validatePathAccess(
|
||||
absolutePath,
|
||||
'read',
|
||||
);
|
||||
|
||||
// We ask the user for explicit permission to read them if outside sandboxed workspace boundaries (and not already authorized).
|
||||
if (
|
||||
validationError &&
|
||||
!isWithinRoot(absolutePath, this.context.config.getTargetDir())
|
||||
) {
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isFile()) {
|
||||
const syntheticCallId = `resolve-prompt-${pathName}-${randomUUID()}`;
|
||||
const params = {
|
||||
sessionId: this.id,
|
||||
options: [
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.ProceedOnce,
|
||||
name: 'Allow once',
|
||||
kind: 'allow_once',
|
||||
},
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.Cancel,
|
||||
name: 'Deny',
|
||||
kind: 'reject_once',
|
||||
},
|
||||
] as acp.PermissionOption[],
|
||||
toolCall: {
|
||||
toolCallId: syntheticCallId,
|
||||
status: 'pending',
|
||||
title: `Allow access to absolute path: ${pathName}`,
|
||||
content: [
|
||||
{
|
||||
type: 'content',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `The Agent needs access to read an attached file outside your workspace: ${pathName}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
locations: [],
|
||||
kind: 'read',
|
||||
},
|
||||
};
|
||||
|
||||
const output = RequestPermissionResponseSchema.parse(
|
||||
await this.connection.requestPermission(params),
|
||||
);
|
||||
|
||||
const outcome =
|
||||
output.outcome.outcome === 'cancelled'
|
||||
? ToolConfirmationOutcome.Cancel
|
||||
: z
|
||||
.nativeEnum(ToolConfirmationOutcome)
|
||||
.parse(output.outcome.optionId);
|
||||
|
||||
if (outcome === ToolConfirmationOutcome.ProceedOnce) {
|
||||
this.context.config
|
||||
.getWorkspaceContext()
|
||||
.addReadOnlyPath(absolutePath);
|
||||
validationError = null;
|
||||
} else {
|
||||
this.debug(
|
||||
`Direct read authorization denied for absolute path ${pathName}`,
|
||||
);
|
||||
directContents.push({
|
||||
spec: pathName,
|
||||
content: `[Warning: Access to absolute path \`${pathName}\` denied by user.]`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isWithinRoot(absolutePath, this.context.config.getTargetDir())) {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isDirectory()) {
|
||||
currentPathSpec = pathName.endsWith('/')
|
||||
? `${pathName}**`
|
||||
: `${pathName}/**`;
|
||||
this.debug(
|
||||
`Failed to request permission for absolute attachment ${pathName}: ${getErrorMessage(error)}`,
|
||||
`Path ${pathName} resolved to directory, using glob: ${currentPathSpec}`,
|
||||
);
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Warning: Failed to display permission dialog for \`${absolutePath}\`. Error: ${getErrorMessage(error)}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!validationError) {
|
||||
// If it's an absolute path that is authorized (e.g. added via readOnlyPaths),
|
||||
// read it directly to avoid ReadManyFilesTool absolute path resolution issues.
|
||||
if (
|
||||
(path.isAbsolute(pathName) ||
|
||||
!isWithinRoot(
|
||||
absolutePath,
|
||||
this.context.config.getTargetDir(),
|
||||
)) &&
|
||||
!readDirectly
|
||||
) {
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isFile()) {
|
||||
const fileReadResult = await processSingleFileContent(
|
||||
absolutePath,
|
||||
this.context.config.getTargetDir(),
|
||||
this.context.config.getFileSystemService(),
|
||||
);
|
||||
|
||||
if (!fileReadResult.error) {
|
||||
if (
|
||||
typeof fileReadResult.llmContent === 'object' &&
|
||||
'inlineData' in fileReadResult.llmContent
|
||||
) {
|
||||
directContents.push({
|
||||
spec: pathName,
|
||||
part: fileReadResult.llmContent,
|
||||
});
|
||||
} else if (typeof fileReadResult.llmContent === 'string') {
|
||||
let contentToPush = fileReadResult.llmContent;
|
||||
if (fileReadResult.isTruncated) {
|
||||
contentToPush = `[WARNING: This file was truncated]\n\n${contentToPush}`;
|
||||
}
|
||||
directContents.push({
|
||||
spec: pathName,
|
||||
content: contentToPush,
|
||||
});
|
||||
}
|
||||
readDirectly = true;
|
||||
resolvedSuccessfully = true;
|
||||
} else {
|
||||
this.debug(
|
||||
`Direct read failed for absolute path ${pathName}: ${fileReadResult.error}`,
|
||||
);
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Warning: file read failed for \`${pathName}\`. Reason: ${fileReadResult.error}`,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.debug(
|
||||
`File stat/access error for absolute path ${pathName}: ${getErrorMessage(error)}`,
|
||||
);
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Warning: file access failed for \`${pathName}\`. Reason: ${getErrorMessage(error)}`,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!readDirectly) {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isDirectory()) {
|
||||
currentPathSpec = pathName.endsWith('/')
|
||||
? `${pathName}**`
|
||||
: `${pathName}/**`;
|
||||
this.debug(
|
||||
`Path ${pathName} resolved to directory, using glob: ${currentPathSpec}`,
|
||||
);
|
||||
} else {
|
||||
this.debug(
|
||||
`Path ${pathName} resolved to file: ${currentPathSpec}`,
|
||||
);
|
||||
}
|
||||
resolvedSuccessfully = true;
|
||||
} else {
|
||||
this.debug(`Path ${pathName} resolved to file: ${currentPathSpec}`);
|
||||
}
|
||||
resolvedSuccessfully = true;
|
||||
} else {
|
||||
this.debug(
|
||||
`Path ${pathName} access disallowed: ${validationError}. Skipping.`,
|
||||
`Path ${pathName} is outside the project directory. Skipping.`,
|
||||
);
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Warning: skipping access to \`${pathName}\`. Reason: ${validationError}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
@@ -1516,9 +1328,7 @@ export class Session {
|
||||
}
|
||||
}
|
||||
if (resolvedSuccessfully) {
|
||||
if (!readDirectly) {
|
||||
pathSpecsToRead.push(currentPathSpec);
|
||||
}
|
||||
pathSpecsToRead.push(currentPathSpec);
|
||||
atPathToResolvedSpecMap.set(pathName, currentPathSpec);
|
||||
contentLabelsForDisplay.push(pathName);
|
||||
}
|
||||
@@ -1579,11 +1389,7 @@ export class Session {
|
||||
|
||||
const processedQueryParts: Part[] = [{ text: initialQueryText }];
|
||||
|
||||
if (
|
||||
pathSpecsToRead.length === 0 &&
|
||||
embeddedContext.length === 0 &&
|
||||
directContents.length === 0
|
||||
) {
|
||||
if (pathSpecsToRead.length === 0 && embeddedContext.length === 0) {
|
||||
// Fallback for lone "@" or completely invalid @-commands resulting in empty initialQueryText
|
||||
debugLogger.warn('No valid file paths found in @ commands to read.');
|
||||
return [{ text: initialQueryText }];
|
||||
@@ -1675,30 +1481,6 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
if (directContents.length > 0) {
|
||||
const hasReferenceStart = processedQueryParts.some(
|
||||
(p) =>
|
||||
'text' in p &&
|
||||
typeof p.text === 'string' &&
|
||||
p.text.includes(REFERENCE_CONTENT_START),
|
||||
);
|
||||
if (!hasReferenceStart) {
|
||||
processedQueryParts.push({
|
||||
text: `\n${REFERENCE_CONTENT_START}`,
|
||||
});
|
||||
}
|
||||
for (const item of directContents) {
|
||||
processedQueryParts.push({
|
||||
text: `\nContent from @${item.spec}:\n`,
|
||||
});
|
||||
if (item.content) {
|
||||
processedQueryParts.push({ text: item.content });
|
||||
} else if (item.part) {
|
||||
processedQueryParts.push(item.part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (embeddedContext.length > 0) {
|
||||
processedQueryParts.push({
|
||||
text: '\n--- Content from referenced context ---',
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'docker',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -122,7 +122,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'lxc',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -148,7 +148,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'sandbox-exec',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -161,7 +161,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'sandbox-exec',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -174,7 +174,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'docker',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -187,7 +187,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'podman',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -210,7 +210,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'podman',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -244,7 +244,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'docker',
|
||||
image: 'env/image',
|
||||
});
|
||||
@@ -257,7 +257,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'docker',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -285,7 +285,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'docker',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -339,7 +339,7 @@ describe('loadSandboxConfig', () => {
|
||||
enabled: true,
|
||||
command: 'podman',
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -356,7 +356,7 @@ describe('loadSandboxConfig', () => {
|
||||
enabled: true,
|
||||
image: 'custom/image',
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -372,7 +372,7 @@ describe('loadSandboxConfig', () => {
|
||||
sandbox: {
|
||||
enabled: false,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -388,7 +388,7 @@ describe('loadSandboxConfig', () => {
|
||||
sandbox: {
|
||||
enabled: true,
|
||||
allowedPaths: ['/settings-path'],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -410,7 +410,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'runsc',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -425,7 +425,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'runsc',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -442,7 +442,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'runsc',
|
||||
image: 'default/image',
|
||||
});
|
||||
@@ -460,7 +460,7 @@ describe('loadSandboxConfig', () => {
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
networkAccess: true,
|
||||
command: 'runsc',
|
||||
image: 'default/image',
|
||||
});
|
||||
|
||||
@@ -131,7 +131,7 @@ export async function loadSandboxConfig(
|
||||
|
||||
let sandboxValue: boolean | string | null | undefined;
|
||||
let allowedPaths: string[] = [];
|
||||
let networkAccess = false;
|
||||
let networkAccess = true;
|
||||
let customImage: string | undefined;
|
||||
|
||||
if (
|
||||
@@ -142,7 +142,7 @@ export async function loadSandboxConfig(
|
||||
const config = sandboxOption;
|
||||
sandboxValue = config.enabled ? (config.command ?? true) : false;
|
||||
allowedPaths = config.allowedPaths ?? [];
|
||||
networkAccess = config.networkAccess ?? false;
|
||||
networkAccess = config.networkAccess ?? true;
|
||||
customImage = config.image;
|
||||
} else if (typeof sandboxOption !== 'object' || sandboxOption === null) {
|
||||
sandboxValue = sandboxOption;
|
||||
|
||||
@@ -300,7 +300,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: undefined as string | undefined,
|
||||
description:
|
||||
'The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode.',
|
||||
'The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory.',
|
||||
showInDialog: true,
|
||||
},
|
||||
modelRouting: {
|
||||
@@ -1208,16 +1208,6 @@ const SETTINGS_SCHEMA = {
|
||||
'Disable user input on browser window during automation.',
|
||||
showInDialog: false,
|
||||
},
|
||||
maxActionsPerTask: {
|
||||
type: 'number',
|
||||
label: 'Max Actions Per Task',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: 100,
|
||||
description:
|
||||
'The maximum number of tool calls allowed per browser task. Enforcement is hard: the agent will be terminated when the limit is reached.',
|
||||
showInDialog: false,
|
||||
},
|
||||
confirmSensitiveActions: {
|
||||
type: 'boolean',
|
||||
label: 'Confirm Sensitive Actions',
|
||||
@@ -2008,7 +1998,7 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'JIT Context Loading',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
default: false,
|
||||
description: 'Enable Just-In-Time (JIT) context loading.',
|
||||
showInDialog: false,
|
||||
},
|
||||
@@ -3024,6 +3014,7 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
|
||||
type: 'object',
|
||||
properties: {
|
||||
useGemini3_1: { type: 'boolean' },
|
||||
useGemini3_1FlashLite: { type: 'boolean' },
|
||||
useCustomTools: { type: 'boolean' },
|
||||
hasAccessToPreview: { type: 'boolean' },
|
||||
requestedModels: {
|
||||
|
||||
@@ -528,62 +528,6 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should call process.stdin.resume when isInteractive is true to protect against implicit Node pause', async () => {
|
||||
const resumeSpy = vi.spyOn(process.stdin, 'resume');
|
||||
vi.mocked(loadCliConfig).mockResolvedValue(
|
||||
createMockConfig({
|
||||
isInteractive: () => true,
|
||||
getQuestion: () => '',
|
||||
getSandbox: () => undefined,
|
||||
}),
|
||||
);
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: {
|
||||
advanced: {},
|
||||
security: { auth: {} },
|
||||
ui: {},
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
model: undefined,
|
||||
sandbox: undefined,
|
||||
debug: undefined,
|
||||
prompt: undefined,
|
||||
promptInteractive: undefined,
|
||||
query: undefined,
|
||||
yolo: undefined,
|
||||
approvalMode: undefined,
|
||||
policy: undefined,
|
||||
adminPolicy: undefined,
|
||||
allowedMcpServerNames: undefined,
|
||||
allowedTools: undefined,
|
||||
experimentalAcp: undefined,
|
||||
extensions: undefined,
|
||||
listExtensions: undefined,
|
||||
includeDirectories: undefined,
|
||||
screenReader: undefined,
|
||||
useWriteTodos: undefined,
|
||||
resume: undefined,
|
||||
listSessions: undefined,
|
||||
deleteSession: undefined,
|
||||
outputFormat: undefined,
|
||||
fakeResponses: undefined,
|
||||
recordResponses: undefined,
|
||||
rawOutput: undefined,
|
||||
acceptRawOutputRisk: undefined,
|
||||
isCommand: undefined,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await main();
|
||||
});
|
||||
|
||||
expect(resumeSpy).toHaveBeenCalledTimes(1);
|
||||
resumeSpy.mockRestore();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ flag: 'listExtensions' },
|
||||
{ flag: 'listSessions' },
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
ValidationRequiredError,
|
||||
type AdminControlsSettings,
|
||||
debugLogger,
|
||||
isHeadlessMode,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import { loadCliConfig, parseArguments } from './config/config.js';
|
||||
@@ -297,7 +296,6 @@ export async function main() {
|
||||
const isDebugMode = cliConfig.isDebugMode(argv);
|
||||
const consolePatcher = new ConsolePatcher({
|
||||
stderr: true,
|
||||
interactive: isHeadlessMode() ? false : true,
|
||||
debugMode: isDebugMode,
|
||||
onNewMessage: (msg) => {
|
||||
coreEvents.emitConsoleLog(msg.type, msg.content);
|
||||
@@ -613,17 +611,8 @@ export async function main() {
|
||||
}
|
||||
|
||||
cliStartupHandle?.end();
|
||||
|
||||
// Render UI, passing necessary config values. Check that there is no command line question.
|
||||
if (config.isInteractive()) {
|
||||
// Earlier initialization phases (like TerminalCapabilityManager resolving
|
||||
// or authWithWeb) may have added and removed 'data' listeners on process.stdin.
|
||||
// When the listener count drops to 0, Node.js implicitly pauses the stream buffer.
|
||||
// React Ink's useInput hooks will silently fail to receive keystrokes if the stream remains paused.
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.resume();
|
||||
}
|
||||
|
||||
await startInteractiveUI(
|
||||
config,
|
||||
settings,
|
||||
|
||||
@@ -46,6 +46,7 @@ import { TerminalProvider } from './ui/contexts/TerminalContext.js';
|
||||
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
|
||||
import { OverflowProvider } from './ui/contexts/OverflowContext.js';
|
||||
import { profiler } from './ui/components/DebugProfiler.js';
|
||||
import { initializeConsoleStore } from './ui/hooks/useConsoleMessages.js';
|
||||
|
||||
const SLOW_RENDER_MS = 200;
|
||||
|
||||
@@ -57,6 +58,7 @@ export async function startInteractiveUI(
|
||||
resumedSessionData: ResumedSessionData | undefined,
|
||||
initializationResult: InitializationResult,
|
||||
) {
|
||||
initializeConsoleStore();
|
||||
// Never enter Ink alternate buffer mode when screen reader mode is enabled
|
||||
// as there is no benefit of alternate buffer mode when using a screen reader
|
||||
// and the Ink alternate buffer mode requires line wrapping harmful to
|
||||
|
||||
@@ -65,7 +65,6 @@ export async function runNonInteractive({
|
||||
return promptIdContext.run(prompt_id, async () => {
|
||||
const consolePatcher = new ConsolePatcher({
|
||||
stderr: true,
|
||||
interactive: false,
|
||||
debugMode: config.getDebugMode(),
|
||||
onNewMessage: (msg) => {
|
||||
coreEvents.emitConsoleLog(msg.type, msg.content);
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
} from 'react';
|
||||
import { useMemo, useState, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
type DOMElement,
|
||||
ResizeObserver,
|
||||
@@ -1392,23 +1385,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
(streamingState === StreamingState.Idle ||
|
||||
streamingState === StreamingState.Responding ||
|
||||
streamingState === StreamingState.WaitingForConfirmation) &&
|
||||
!proQuotaRequest &&
|
||||
!copyModeEnabled;
|
||||
!proQuotaRequest;
|
||||
|
||||
const observerRef = useRef<ResizeObserver | null>(null);
|
||||
const [controlsHeight, setControlsHeight] = useState(0);
|
||||
const [lastNonCopyControlsHeight, setLastNonCopyControlsHeight] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!copyModeEnabled && controlsHeight > 0) {
|
||||
setLastNonCopyControlsHeight(controlsHeight);
|
||||
}
|
||||
}, [copyModeEnabled, controlsHeight]);
|
||||
|
||||
const stableControlsHeight =
|
||||
copyModeEnabled && lastNonCopyControlsHeight > 0
|
||||
? lastNonCopyControlsHeight
|
||||
: controlsHeight;
|
||||
|
||||
const mainControlsRef = useCallback((node: DOMElement | null) => {
|
||||
if (observerRef.current) {
|
||||
@@ -1431,10 +1411,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Compute available terminal height based on stable controls measurement
|
||||
// Compute available terminal height based on controls measurement
|
||||
const availableTerminalHeight = Math.max(
|
||||
0,
|
||||
terminalHeight - stableControlsHeight - backgroundShellHeight - 1,
|
||||
terminalHeight - controlsHeight - backgroundShellHeight - 1,
|
||||
);
|
||||
|
||||
config.setShellExecutionConfig({
|
||||
@@ -2293,7 +2273,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
contextFileNames,
|
||||
errorCount,
|
||||
availableTerminalHeight,
|
||||
stableControlsHeight,
|
||||
mainAreaWidth,
|
||||
staticAreaMaxItemHeight,
|
||||
staticExtraHeight,
|
||||
@@ -2415,7 +2394,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
contextFileNames,
|
||||
errorCount,
|
||||
availableTerminalHeight,
|
||||
stableControlsHeight,
|
||||
mainAreaWidth,
|
||||
staticAreaMaxItemHeight,
|
||||
staticExtraHeight,
|
||||
|
||||
@@ -37,8 +37,9 @@ Tips for getting started:
|
||||
|
||||
|
||||
|
||||
Notifications
|
||||
|
||||
|
||||
Notifications
|
||||
Composer
|
||||
"
|
||||
`;
|
||||
@@ -102,8 +103,9 @@ exports[`App > Snapshots > renders with dialogs visible 1`] = `
|
||||
|
||||
|
||||
|
||||
Notifications
|
||||
|
||||
|
||||
Notifications
|
||||
DialogManager
|
||||
"
|
||||
`;
|
||||
@@ -145,8 +147,9 @@ HistoryItemDisplay
|
||||
|
||||
|
||||
|
||||
Notifications
|
||||
|
||||
|
||||
Notifications
|
||||
Composer
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -1,266 +1,271 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="666" viewBox="0 0 920 666">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="700" viewBox="0 0 920 700">
|
||||
<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" />
|
||||
<rect width="920" height="700" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<rect x="0" y="0" width="900" height="17" fill="#141414" />
|
||||
<text x="0" y="2" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀</text>
|
||||
<rect x="0" y="17" width="9" height="17" fill="#141414" />
|
||||
<rect x="9" y="17" width="18" height="17" fill="#141414" />
|
||||
<text x="9" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<rect x="27" y="17" width="324" height="17" fill="#141414" />
|
||||
<text x="27" y="19" fill="#ffffff" textLength="324" lengthAdjust="spacingAndGlyphs">Can you edit InputPrompt.tsx for me?</text>
|
||||
<rect x="351" y="17" width="549" height="17" fill="#141414" />
|
||||
<text x="0" y="2" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
|
||||
<text x="0" y="19" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
|
||||
<rect x="0" y="34" width="900" height="17" fill="#141414" />
|
||||
<text x="0" y="36" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄</text>
|
||||
<text x="0" y="53" fill="#ffffaf" textLength="891" lengthAdjust="spacingAndGlyphs">╭─────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="0" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffaf" textLength="135" lengthAdjust="spacingAndGlyphs" font-weight="bold">Action Required</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="882" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀</text>
|
||||
<rect x="0" y="51" width="9" height="17" fill="#141414" />
|
||||
<rect x="9" y="51" width="18" height="17" fill="#141414" />
|
||||
<text x="9" y="53" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<rect x="27" y="51" width="324" height="17" fill="#141414" />
|
||||
<text x="27" y="53" fill="#ffffff" textLength="324" lengthAdjust="spacingAndGlyphs">Can you edit InputPrompt.tsx for me?</text>
|
||||
<rect x="351" y="51" width="549" height="17" fill="#141414" />
|
||||
<rect x="0" y="68" width="900" height="17" fill="#141414" />
|
||||
<text x="0" y="70" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄</text>
|
||||
<text x="0" y="87" fill="#ffffaf" textLength="891" lengthAdjust="spacingAndGlyphs">╭─────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="0" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">?</text>
|
||||
<text x="45" y="104" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs" font-weight="bold">Edit</text>
|
||||
<text x="90" y="104" fill="#afafaf" textLength="774" lengthAdjust="spacingAndGlyphs">packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto</text>
|
||||
<text x="864" y="104" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">… </text>
|
||||
<text x="18" y="104" fill="#ffffaf" textLength="135" lengthAdjust="spacingAndGlyphs" font-weight="bold">Action Required</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="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="414" lengthAdjust="spacingAndGlyphs">... first 44 lines hidden (Ctrl+O to show) ...</text>
|
||||
<text x="18" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">?</text>
|
||||
<text x="45" y="138" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs" font-weight="bold">Edit</text>
|
||||
<text x="90" y="138" fill="#afafaf" textLength="774" lengthAdjust="spacingAndGlyphs">packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto</text>
|
||||
<text x="864" y="138" fill="#ffffff" textLength="18" 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">45</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">line45</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">46</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">line46</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="18" y="172" fill="#afafaf" textLength="414" lengthAdjust="spacingAndGlyphs">... first 44 lines hidden (Ctrl+O to show) ...</text>
|
||||
<text x="882" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="172" fill="#333333" 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">47</text>
|
||||
<text x="18" y="189" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">45</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">line47</text>
|
||||
<text x="117" y="189" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line45</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="891" y="189" fill="#333333" 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">48</text>
|
||||
<text x="18" y="206" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">46</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">line48</text>
|
||||
<text x="117" y="206" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line46</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="891" y="206" fill="#333333" 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">49</text>
|
||||
<text x="18" y="223" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">47</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">line49</text>
|
||||
<text x="117" y="223" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line47</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="891" y="223" fill="#333333" 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">50</text>
|
||||
<text x="18" y="240" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">48</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">line50</text>
|
||||
<text x="117" y="240" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line48</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="891" y="240" fill="#333333" 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">51</text>
|
||||
<text x="18" y="257" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">49</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">line51</text>
|
||||
<text x="117" y="257" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line49</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="891" y="257" fill="#333333" 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">52</text>
|
||||
<text x="18" y="274" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">50</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">line52</text>
|
||||
<text x="117" y="274" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line50</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="891" y="274" fill="#333333" 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">53</text>
|
||||
<text x="18" y="291" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">51</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">line53</text>
|
||||
<text x="117" y="291" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line51</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="891" y="291" fill="#333333" 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">54</text>
|
||||
<text x="18" y="308" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">52</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">line54</text>
|
||||
<text x="117" y="308" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line52</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="891" y="308" fill="#333333" 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">55</text>
|
||||
<text x="18" y="325" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">53</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">line55</text>
|
||||
<text x="117" y="325" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line53</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="891" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="342" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">56</text>
|
||||
<text x="18" y="342" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">54</text>
|
||||
<text x="63" y="342" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="342" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line56</text>
|
||||
<text x="117" y="342" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line54</text>
|
||||
<text x="171" y="342" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="342" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="342" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="359" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">57</text>
|
||||
<text x="18" y="359" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">55</text>
|
||||
<text x="63" y="359" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="359" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line57</text>
|
||||
<text x="117" y="359" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line55</text>
|
||||
<text x="171" y="359" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="359" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="359" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="359" fill="#333333" 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">58</text>
|
||||
<text x="18" y="376" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">56</text>
|
||||
<text x="63" y="376" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="376" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line58</text>
|
||||
<text x="117" y="376" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line56</text>
|
||||
<text x="171" y="376" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="376" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="376" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="376" fill="#333333" 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">59</text>
|
||||
<text x="18" y="393" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">57</text>
|
||||
<text x="63" y="393" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="393" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line59</text>
|
||||
<text x="117" y="393" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line57</text>
|
||||
<text x="171" y="393" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="393" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="393" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="410" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">60</text>
|
||||
<text x="18" y="410" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">58</text>
|
||||
<text x="63" y="410" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="410" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line60</text>
|
||||
<text x="117" y="410" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line58</text>
|
||||
<text x="171" y="410" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="410" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="410" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="410" fill="#333333" 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">61</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="9" height="17" fill="#5f0000" />
|
||||
<rect x="72" y="425" width="54" height="17" fill="#5f0000" />
|
||||
<text x="72" y="427" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
|
||||
<rect x="126" y="425" width="234" height="17" fill="#5f0000" />
|
||||
<text x="126" y="427" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</text>
|
||||
<text x="18" y="427" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">59</text>
|
||||
<text x="63" y="427" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="427" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line59</text>
|
||||
<text x="171" y="427" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="427" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="427" fill="#00cd00" 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="18" height="17" fill="#005f00" />
|
||||
<text x="18" y="444" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">61</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="9" height="17" fill="#005f00" />
|
||||
<rect x="72" y="442" width="54" height="17" fill="#005f00" />
|
||||
<text x="72" y="444" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
|
||||
<rect x="126" y="442" width="234" height="17" fill="#005f00" />
|
||||
<text x="126" y="444" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</text>
|
||||
<text x="18" y="444" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">60</text>
|
||||
<text x="63" y="444" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="444" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line60</text>
|
||||
<text x="171" y="444" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="444" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="444" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<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="18" y="461" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">62</text>
|
||||
<text x="63" y="461" fill="#e5e5e5" textLength="180" lengthAdjust="spacingAndGlyphs"> buffer: TextBuffer;</text>
|
||||
<rect x="18" y="459" width="18" height="17" fill="#5f0000" />
|
||||
<text x="18" y="461" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">61</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="9" height="17" fill="#5f0000" />
|
||||
<rect x="72" y="459" width="54" height="17" fill="#5f0000" />
|
||||
<text x="72" y="461" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
|
||||
<rect x="126" y="459" width="234" height="17" fill="#5f0000" />
|
||||
<text x="126" y="461" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</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="18" y="478" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">63</text>
|
||||
<text x="72" y="478" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">onSubmit</text>
|
||||
<text x="144" y="478" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs">: (</text>
|
||||
<text x="171" y="478" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">value</text>
|
||||
<text x="216" y="478" fill="#e5e5e5" textLength="18" lengthAdjust="spacingAndGlyphs">: </text>
|
||||
<text x="234" y="478" fill="#00cdcd" textLength="54" lengthAdjust="spacingAndGlyphs">string</text>
|
||||
<text x="288" y="478" fill="#e5e5e5" textLength="45" lengthAdjust="spacingAndGlyphs">) => </text>
|
||||
<text x="333" y="478" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">void</text>
|
||||
<text x="369" y="478" fill="#e5e5e5" 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">61</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="9" height="17" fill="#005f00" />
|
||||
<rect x="72" y="476" width="54" height="17" fill="#005f00" />
|
||||
<text x="72" y="478" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
|
||||
<rect x="126" y="476" width="234" height="17" fill="#005f00" />
|
||||
<text x="126" y="478" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</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="18" y="495" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Apply this change?</text>
|
||||
<text x="18" y="495" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">62</text>
|
||||
<text x="63" y="495" fill="#e5e5e5" textLength="180" lengthAdjust="spacingAndGlyphs"> buffer: TextBuffer;</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="18" y="512" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">63</text>
|
||||
<text x="72" y="512" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">onSubmit</text>
|
||||
<text x="144" y="512" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs">: (</text>
|
||||
<text x="171" y="512" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">value</text>
|
||||
<text x="216" y="512" fill="#e5e5e5" textLength="18" lengthAdjust="spacingAndGlyphs">: </text>
|
||||
<text x="234" y="512" fill="#00cdcd" textLength="54" lengthAdjust="spacingAndGlyphs">string</text>
|
||||
<text x="288" y="512" fill="#e5e5e5" textLength="45" lengthAdjust="spacingAndGlyphs">) => </text>
|
||||
<text x="333" y="512" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">void</text>
|
||||
<text x="369" y="512" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</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>
|
||||
<rect x="18" y="527" width="9" height="17" fill="#001a00" />
|
||||
<text x="18" y="529" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="27" y="527" width="9" height="17" fill="#001a00" />
|
||||
<rect x="36" y="527" width="18" height="17" fill="#001a00" />
|
||||
<text x="36" y="529" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
|
||||
<rect x="54" y="527" width="9" height="17" fill="#001a00" />
|
||||
<rect x="63" y="527" width="90" height="17" fill="#001a00" />
|
||||
<text x="63" y="529" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
|
||||
<rect x="153" y="527" width="288" height="17" fill="#001a00" />
|
||||
<text x="18" y="529" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Apply this change?</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="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="546" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">2.</text>
|
||||
<text x="63" y="546" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Allow for this session</text>
|
||||
<text x="882" y="546" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="563" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="563" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
|
||||
<text x="63" y="563" fill="#ffffff" textLength="378" lengthAdjust="spacingAndGlyphs">Allow for this file in all future sessions</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="288" height="17" fill="#001a00" />
|
||||
<text x="882" y="563" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="563" fill="#333333" 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">4.</text>
|
||||
<text x="63" y="580" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Modify with external editor</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="882" y="580" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="580" fill="#333333" 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">5.</text>
|
||||
<text x="63" y="597" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
|
||||
<text x="36" y="597" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
|
||||
<text x="63" y="597" fill="#ffffff" textLength="378" lengthAdjust="spacingAndGlyphs">Allow for this file in all future sessions</text>
|
||||
<text x="882" y="597" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="597" fill="#333333" 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="243" lengthAdjust="spacingAndGlyphs">Modify with external editor</text>
|
||||
<text x="882" y="614" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="631" fill="#ffffaf" textLength="891" lengthAdjust="spacingAndGlyphs">╰─────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="0" y="631" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="631" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">5.</text>
|
||||
<text x="63" y="631" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
|
||||
<text x="882" y="631" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="648" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="648" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="665" fill="#ffffaf" textLength="891" lengthAdjust="spacingAndGlyphs">╰─────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
@@ -1,7 +1,9 @@
|
||||
// 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`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
"3. Ask coding questions, edit code or run commands
|
||||
4. Be specific for the best results
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Can you edit InputPrompt.tsx for me?
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
@@ -9,9 +11,9 @@ exports[`Full Terminal Tool Confirmation Snapshot > renders tool confirmation bo
|
||||
│ │
|
||||
│ ? Edit packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto… │
|
||||
│ │
|
||||
│ ... first 44 lines hidden (Ctrl+O to show) ... │
|
||||
│ 45 const line45 = true; │
|
||||
│ 46 const line46 = true; │
|
||||
│ ... first 44 lines hidden (Ctrl+O to show) ... │█
|
||||
│ 45 const line45 = true; │█
|
||||
│ 46 const line46 = true; │█
|
||||
│ 47 const line47 = true; │█
|
||||
│ 48 const line48 = true; │█
|
||||
│ 49 const line49 = true; │█
|
||||
|
||||
@@ -108,7 +108,7 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
|
||||
Gemini CLI
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}> v{version}</Text>
|
||||
{updateInfo?.isUpdating && (
|
||||
{updateInfo && (
|
||||
<Box marginLeft={2}>
|
||||
<Text color={theme.text.secondary}>
|
||||
<CliSpinner /> Updating
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, useIsScreenReaderEnabled } from 'ink';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
ApprovalMode,
|
||||
checkExhaustive,
|
||||
CoreToolCallStatus,
|
||||
isUserVisibleHook,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
@@ -14,18 +20,28 @@ import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||
import { isContextUsageHigh } from '../utils/contextUsage.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { GENERIC_WORKING_LABEL } from '../textConstants.js';
|
||||
import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js';
|
||||
import { StreamingState, type HistoryItemToolGroup } from '../types.js';
|
||||
import { LoadingIndicator } from './LoadingIndicator.js';
|
||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||
import { StatusDisplay } from './StatusDisplay.js';
|
||||
import { HorizontalLine } from './shared/HorizontalLine.js';
|
||||
import { ToastDisplay, shouldShowToast } from './ToastDisplay.js';
|
||||
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
|
||||
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
||||
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
|
||||
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
|
||||
import { ShortcutsHelp } from './ShortcutsHelp.js';
|
||||
import { InputPrompt } from './InputPrompt.js';
|
||||
import { Footer } from './Footer.js';
|
||||
import { StatusRow } from './StatusRow.js';
|
||||
import { ShowMoreLines } from './ShowMoreLines.js';
|
||||
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
|
||||
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
||||
import { ConfigInitDisplay } from './ConfigInitDisplay.js';
|
||||
import { TodoTray } from './messages/Todo.js';
|
||||
import { useComposerStatus } from '../hooks/useComposerStatus.js';
|
||||
|
||||
export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
const uiState = useUIState();
|
||||
@@ -40,17 +56,43 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
const [suggestionsVisible, setSuggestionsVisible] = useState(false);
|
||||
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const showApprovalModeIndicator = uiState.showApprovalModeIndicator;
|
||||
const loadingPhrases = settings.merged.ui.loadingPhrases;
|
||||
const showTips = loadingPhrases === 'tips' || loadingPhrases === 'all';
|
||||
const showWit = loadingPhrases === 'witty' || loadingPhrases === 'all';
|
||||
|
||||
const showUiDetails = uiState.cleanUiDetailsVisible;
|
||||
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
|
||||
const hideContextSummary =
|
||||
suggestionsVisible && suggestionsPosition === 'above';
|
||||
|
||||
const { hasPendingActionRequired, shouldCollapseDuringApproval } =
|
||||
useComposerStatus();
|
||||
const hasPendingToolConfirmation = useMemo(
|
||||
() =>
|
||||
(uiState.pendingHistoryItems ?? [])
|
||||
.filter(
|
||||
(item): item is HistoryItemToolGroup => item.type === 'tool_group',
|
||||
)
|
||||
.some((item) =>
|
||||
item.tools.some(
|
||||
(tool) => tool.status === CoreToolCallStatus.AwaitingApproval,
|
||||
),
|
||||
),
|
||||
[uiState.pendingHistoryItems],
|
||||
);
|
||||
|
||||
const hasPendingActionRequired =
|
||||
hasPendingToolConfirmation ||
|
||||
Boolean(uiState.commandConfirmationRequest) ||
|
||||
Boolean(uiState.authConsentRequest) ||
|
||||
(uiState.confirmUpdateExtensionRequests?.length ?? 0) > 0 ||
|
||||
Boolean(uiState.loopDetectionConfirmationRequest) ||
|
||||
Boolean(uiState.quota.proQuotaRequest) ||
|
||||
Boolean(uiState.quota.validationRequest) ||
|
||||
Boolean(uiState.customDialog);
|
||||
|
||||
const isPassiveShortcutsHelpState =
|
||||
uiState.isInputActive &&
|
||||
uiState.streamingState === 'idle' &&
|
||||
uiState.streamingState === StreamingState.Idle &&
|
||||
!hasPendingActionRequired;
|
||||
|
||||
const { setShortcutsHelpVisible } = uiActions;
|
||||
@@ -67,19 +109,407 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
|
||||
const showShortcutsHelp =
|
||||
uiState.shortcutsHelpVisible &&
|
||||
uiState.streamingState === 'idle' &&
|
||||
uiState.streamingState === StreamingState.Idle &&
|
||||
!hasPendingActionRequired;
|
||||
|
||||
/**
|
||||
* Use the setting if provided, otherwise default to true for the new UX.
|
||||
* This allows tests to override the collapse behavior.
|
||||
*/
|
||||
const shouldCollapseDuringApproval =
|
||||
settings.merged.ui.collapseDrawerDuringApproval !== false;
|
||||
|
||||
if (hasPendingActionRequired && shouldCollapseDuringApproval) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasToast = shouldShowToast(uiState);
|
||||
const showLoadingIndicator =
|
||||
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
|
||||
uiState.streamingState === StreamingState.Responding &&
|
||||
!hasPendingActionRequired;
|
||||
|
||||
const hideUiDetailsForSuggestions =
|
||||
suggestionsVisible && suggestionsPosition === 'above';
|
||||
const showApprovalIndicator =
|
||||
!uiState.shellModeActive && !hideUiDetailsForSuggestions;
|
||||
const showRawMarkdownIndicator = !uiState.renderMarkdown;
|
||||
|
||||
let modeBleedThrough: { text: string; color: string } | null = null;
|
||||
switch (showApprovalModeIndicator) {
|
||||
case ApprovalMode.YOLO:
|
||||
modeBleedThrough = { text: 'YOLO', color: theme.status.error };
|
||||
break;
|
||||
case ApprovalMode.PLAN:
|
||||
modeBleedThrough = { text: 'plan', color: theme.status.success };
|
||||
break;
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
modeBleedThrough = { text: 'auto edit', color: theme.status.warning };
|
||||
break;
|
||||
case ApprovalMode.DEFAULT:
|
||||
modeBleedThrough = null;
|
||||
break;
|
||||
default:
|
||||
checkExhaustive(showApprovalModeIndicator);
|
||||
modeBleedThrough = null;
|
||||
break;
|
||||
}
|
||||
|
||||
const hideMinimalModeHintWhileBusy =
|
||||
!showUiDetails && (showLoadingIndicator || hasPendingActionRequired);
|
||||
|
||||
// Universal Content Objects
|
||||
const modeContentObj = hideMinimalModeHintWhileBusy ? null : modeBleedThrough;
|
||||
|
||||
const allHooks = uiState.activeHooks;
|
||||
const hasAnyHooks = allHooks.length > 0;
|
||||
const userVisibleHooks = allHooks.filter((h) => isUserVisibleHook(h.source));
|
||||
const hasUserVisibleHooks = userVisibleHooks.length > 0;
|
||||
|
||||
const shouldReserveSpaceForShortcutsHint =
|
||||
settings.merged.ui.showShortcutsHint &&
|
||||
!hideUiDetailsForSuggestions &&
|
||||
!hasPendingActionRequired;
|
||||
|
||||
const isInteractiveShellWaiting = uiState.currentLoadingPhrase?.includes(
|
||||
INTERACTIVE_SHELL_WAITING_PHRASE,
|
||||
);
|
||||
|
||||
/**
|
||||
* Calculate the estimated length of the status message to avoid collisions
|
||||
* with the tips area.
|
||||
*/
|
||||
let estimatedStatusLength = 0;
|
||||
if (hasAnyHooks) {
|
||||
if (hasUserVisibleHooks) {
|
||||
const hookLabel =
|
||||
userVisibleHooks.length > 1 ? 'Executing Hooks' : 'Executing Hook';
|
||||
const hookNames = userVisibleHooks
|
||||
.map(
|
||||
(h) =>
|
||||
h.name +
|
||||
(h.index && h.total && h.total > 1
|
||||
? ` (${h.index}/${h.total})`
|
||||
: ''),
|
||||
)
|
||||
.join(', ');
|
||||
estimatedStatusLength = hookLabel.length + hookNames.length + 10;
|
||||
} else {
|
||||
estimatedStatusLength = GENERIC_WORKING_LABEL.length + 10;
|
||||
}
|
||||
} else if (showLoadingIndicator) {
|
||||
const thoughtText = uiState.thought?.subject || GENERIC_WORKING_LABEL;
|
||||
const inlineWittyLength =
|
||||
showWit && uiState.currentWittyPhrase
|
||||
? uiState.currentWittyPhrase.length + 1
|
||||
: 0;
|
||||
estimatedStatusLength = thoughtText.length + 25 + inlineWittyLength;
|
||||
} else if (hasPendingActionRequired) {
|
||||
estimatedStatusLength = 20;
|
||||
} else if (hasToast) {
|
||||
estimatedStatusLength = 40;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the ambient text (tip) to display.
|
||||
*/
|
||||
const tipContentStr = (() => {
|
||||
// 1. Proactive Tip (Priority)
|
||||
if (
|
||||
showTips &&
|
||||
uiState.currentTip &&
|
||||
!(
|
||||
isInteractiveShellWaiting &&
|
||||
uiState.currentTip === INTERACTIVE_SHELL_WAITING_PHRASE
|
||||
)
|
||||
) {
|
||||
if (
|
||||
estimatedStatusLength + uiState.currentTip.length + 10 <=
|
||||
terminalWidth
|
||||
) {
|
||||
return uiState.currentTip;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Shortcut Hint (Fallback)
|
||||
if (
|
||||
settings.merged.ui.showShortcutsHint &&
|
||||
!hideUiDetailsForSuggestions &&
|
||||
!hasPendingActionRequired &&
|
||||
uiState.buffer.text.length === 0
|
||||
) {
|
||||
return showUiDetails ? '? for shortcuts' : 'press tab twice for more';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
const tipLength = tipContentStr?.length || 0;
|
||||
const willCollideTip = estimatedStatusLength + tipLength + 5 > terminalWidth;
|
||||
|
||||
const showTipLine =
|
||||
!hasPendingActionRequired && tipContentStr && !willCollideTip && !isNarrow;
|
||||
|
||||
// Mini Mode VIP Flags (Pure Content Triggers)
|
||||
const showMinimalToast = hasToast;
|
||||
const miniMode_ShowApprovalMode =
|
||||
Boolean(modeContentObj) && !hideUiDetailsForSuggestions;
|
||||
const miniMode_ShowToast = hasToast;
|
||||
const miniMode_ShowShortcuts = shouldReserveSpaceForShortcutsHint;
|
||||
const miniMode_ShowStatus = showLoadingIndicator || hasAnyHooks;
|
||||
const miniMode_ShowTip = showTipLine;
|
||||
const miniMode_ShowContext = isContextUsageHigh(
|
||||
uiState.sessionStats.lastPromptTokenCount,
|
||||
uiState.currentModel,
|
||||
settings.merged.model?.compressionThreshold,
|
||||
);
|
||||
|
||||
// Composite Mini Mode Triggers
|
||||
const showRow1_MiniMode =
|
||||
miniMode_ShowToast ||
|
||||
miniMode_ShowStatus ||
|
||||
miniMode_ShowShortcuts ||
|
||||
miniMode_ShowTip;
|
||||
|
||||
const showRow2_MiniMode = miniMode_ShowApprovalMode || miniMode_ShowContext;
|
||||
|
||||
// Final Display Rules (Stable Footer Architecture)
|
||||
const showRow1 = showUiDetails || showRow1_MiniMode;
|
||||
const showRow2 = showUiDetails || showRow2_MiniMode;
|
||||
|
||||
const showMinimalBleedThroughRow = !showUiDetails && showRow2_MiniMode;
|
||||
|
||||
const renderTipNode = () => {
|
||||
if (!tipContentStr) return null;
|
||||
|
||||
const isShortcutHint =
|
||||
tipContentStr === '? for shortcuts' ||
|
||||
tipContentStr === 'press tab twice for more';
|
||||
const color =
|
||||
isShortcutHint && uiState.shortcutsHelpVisible
|
||||
? theme.text.accent
|
||||
: theme.text.secondary;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" justifyContent="flex-end">
|
||||
<Text
|
||||
color={color}
|
||||
wrap="truncate-end"
|
||||
italic={
|
||||
!isShortcutHint && tipContentStr === uiState.currentWittyPhrase
|
||||
}
|
||||
>
|
||||
{tipContentStr === uiState.currentTip
|
||||
? `Tip: ${tipContentStr}`
|
||||
: tipContentStr}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatusNode = () => {
|
||||
const allHooks = uiState.activeHooks;
|
||||
if (allHooks.length === 0 && !showLoadingIndicator) return null;
|
||||
|
||||
if (allHooks.length > 0) {
|
||||
const userVisibleHooks = allHooks.filter((h) =>
|
||||
isUserVisibleHook(h.source),
|
||||
);
|
||||
|
||||
let hookText = GENERIC_WORKING_LABEL;
|
||||
if (userVisibleHooks.length > 0) {
|
||||
const label =
|
||||
userVisibleHooks.length > 1 ? 'Executing Hooks' : 'Executing Hook';
|
||||
const displayNames = userVisibleHooks.map((h) => {
|
||||
let name = h.name;
|
||||
if (h.index && h.total && h.total > 1) {
|
||||
name += ` (${h.index}/${h.total})`;
|
||||
}
|
||||
return name;
|
||||
});
|
||||
hookText = `${label}: ${displayNames.join(', ')}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<LoadingIndicator
|
||||
inline
|
||||
showTips={showTips}
|
||||
showWit={showWit}
|
||||
errorVerbosity={settings.merged.ui.errorVerbosity}
|
||||
currentLoadingPhrase={hookText}
|
||||
elapsedTime={uiState.elapsedTime}
|
||||
forceRealStatusOnly={false}
|
||||
wittyPhrase={uiState.currentWittyPhrase}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LoadingIndicator
|
||||
inline
|
||||
showTips={showTips}
|
||||
showWit={showWit}
|
||||
errorVerbosity={settings.merged.ui.errorVerbosity}
|
||||
thought={uiState.thought}
|
||||
elapsedTime={uiState.elapsedTime}
|
||||
forceRealStatusOnly={false}
|
||||
wittyPhrase={uiState.currentWittyPhrase}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const statusNode = renderStatusNode();
|
||||
|
||||
/**
|
||||
* Renders the minimal metadata row content shown when UI details are hidden.
|
||||
*/
|
||||
const renderMinimalMetaRowContent = () => (
|
||||
<Box flexDirection="row" columnGap={1}>
|
||||
{renderStatusNode()}
|
||||
{showMinimalBleedThroughRow && (
|
||||
<Box>
|
||||
{miniMode_ShowApprovalMode && modeContentObj && (
|
||||
<Text color={modeContentObj.color}>● {modeContentObj.text}</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderStatusRow = () => {
|
||||
// Mini Mode Height Reservation (The "Anti-Jitter" line)
|
||||
if (!showUiDetails && !showRow1_MiniMode && !showRow2_MiniMode) {
|
||||
return <Box height={1} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width="100%">
|
||||
{/* Row 1: multipurpose status (thinking, hooks, wit, tips) */}
|
||||
{showRow1 && (
|
||||
<Box
|
||||
width="100%"
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
minHeight={1}
|
||||
>
|
||||
<Box flexDirection="row" flexGrow={1} flexShrink={1}>
|
||||
{!showUiDetails && showRow1_MiniMode ? (
|
||||
renderMinimalMetaRowContent()
|
||||
) : isInteractiveShellWaiting ? (
|
||||
<Box width="100%" marginLeft={1}>
|
||||
<Text color={theme.status.warning}>
|
||||
! Shell awaiting input (Tab to focus)
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
flexGrow={1}
|
||||
flexShrink={0}
|
||||
marginLeft={1}
|
||||
>
|
||||
{statusNode}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexShrink={0} marginLeft={2} marginRight={isNarrow ? 0 : 1}>
|
||||
{!isNarrow && showTipLine && renderTipNode()}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Internal Separator Line */}
|
||||
{showRow1 &&
|
||||
showRow2 &&
|
||||
(showUiDetails || (showRow1_MiniMode && showRow2_MiniMode)) && (
|
||||
<Box width="100%">
|
||||
<HorizontalLine dim />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Row 2: Mode and Context Summary */}
|
||||
{showRow2 && (
|
||||
<Box
|
||||
width="100%"
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Box flexDirection="row" alignItems="center" marginLeft={1}>
|
||||
{showUiDetails ? (
|
||||
<>
|
||||
{showApprovalIndicator && (
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={showApprovalModeIndicator}
|
||||
allowPlanMode={uiState.allowPlanMode}
|
||||
/>
|
||||
)}
|
||||
{uiState.shellModeActive && (
|
||||
<Box
|
||||
marginLeft={showApprovalIndicator && !isNarrow ? 1 : 0}
|
||||
marginTop={showApprovalIndicator && isNarrow ? 1 : 0}
|
||||
>
|
||||
<ShellModeIndicator />
|
||||
</Box>
|
||||
)}
|
||||
{showRawMarkdownIndicator && (
|
||||
<Box
|
||||
marginLeft={
|
||||
(showApprovalIndicator || uiState.shellModeActive) &&
|
||||
!isNarrow
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
marginTop={
|
||||
(showApprovalIndicator || uiState.shellModeActive) &&
|
||||
isNarrow
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
>
|
||||
<RawMarkdownIndicator />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
miniMode_ShowApprovalMode &&
|
||||
modeContentObj && (
|
||||
<Text color={modeContentObj.color}>
|
||||
● {modeContentObj.text}
|
||||
</Text>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
marginTop={isNarrow ? 1 : 0}
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
marginLeft={isNarrow ? 1 : 0}
|
||||
>
|
||||
{(showUiDetails || miniMode_ShowContext) && (
|
||||
<StatusDisplay hideContextSummary={hideContextSummary} />
|
||||
)}
|
||||
{miniMode_ShowContext && !showUiDetails && (
|
||||
<Box marginLeft={1}>
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={uiState.sessionStats.lastPromptTokenCount}
|
||||
model={
|
||||
typeof uiState.currentModel === 'string'
|
||||
? uiState.currentModel
|
||||
: undefined
|
||||
}
|
||||
terminalWidth={uiState.terminalWidth}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -100,21 +530,14 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
|
||||
{showShortcutsHelp && <ShortcutsHelp />}
|
||||
|
||||
{(showUiDetails || showMinimalToast) && (
|
||||
{(showUiDetails || miniMode_ShowToast) && (
|
||||
<Box minHeight={1} marginLeft={isNarrow ? 0 : 1}>
|
||||
<ToastDisplay />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box width="100%" flexDirection="column">
|
||||
<StatusRow
|
||||
showUiDetails={showUiDetails}
|
||||
isNarrow={isNarrow}
|
||||
terminalWidth={terminalWidth}
|
||||
hideContextSummary={hideContextSummary}
|
||||
hideUiDetailsForSuggestions={hideUiDetailsForSuggestions}
|
||||
hasPendingActionRequired={hasPendingActionRequired}
|
||||
/>
|
||||
{renderStatusRow()}
|
||||
</Box>
|
||||
|
||||
{showUiDetails && uiState.showErrorDetails && (
|
||||
@@ -146,7 +569,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
commandContext={uiState.commandContext}
|
||||
shellModeActive={uiState.shellModeActive}
|
||||
setShellModeActive={uiActions.setShellModeActive}
|
||||
approvalMode={uiState.showApprovalModeIndicator}
|
||||
approvalMode={showApprovalModeIndicator}
|
||||
onEscapePromptChange={uiActions.onEscapePromptChange}
|
||||
focus={isFocused}
|
||||
vimHandleInput={uiActions.vimHandleInput}
|
||||
@@ -165,15 +588,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
streamingState={uiState.streamingState}
|
||||
suggestionsPosition={suggestionsPosition}
|
||||
onSuggestionsVisibilityChange={setSuggestionsVisible}
|
||||
copyModeEnabled={uiState.copyModeEnabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showUiDetails &&
|
||||
!settings.merged.ui.hideFooter &&
|
||||
!isScreenReaderEnabled && (
|
||||
<Footer copyModeEnabled={uiState.copyModeEnabled} />
|
||||
)}
|
||||
!isScreenReaderEnabled && <Footer />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,14 +12,16 @@ import { theme } from '../semantic-colors.js';
|
||||
export const CopyModeWarning: React.FC = () => {
|
||||
const { copyModeEnabled } = useUIState();
|
||||
|
||||
if (!copyModeEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box height={1}>
|
||||
{copyModeEnabled && (
|
||||
<Text color={theme.status.warning}>
|
||||
In Copy Mode. Use Page Up/Down to scroll. Press Ctrl+S or any other
|
||||
key to exit.
|
||||
</Text>
|
||||
)}
|
||||
<Box>
|
||||
<Text color={theme.status.warning}>
|
||||
In Copy Mode. Use Page Up/Down to scroll. Press Ctrl+S or any other key
|
||||
to exit.
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,10 +35,7 @@ vi.mock('./shared/ScrollableList.js', () => ({
|
||||
|
||||
describe('DetailedMessagesDisplay', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useConsoleMessages).mockReturnValue({
|
||||
consoleMessages: [],
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
vi.mocked(useConsoleMessages).mockReturnValue([]);
|
||||
});
|
||||
it('renders nothing when messages are empty', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
@@ -58,10 +55,7 @@ describe('DetailedMessagesDisplay', () => {
|
||||
{ type: 'error', content: 'Error message', count: 1 },
|
||||
{ type: 'debug', content: 'Debug message', count: 1 },
|
||||
];
|
||||
vi.mocked(useConsoleMessages).mockReturnValue({
|
||||
consoleMessages: messages,
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
vi.mocked(useConsoleMessages).mockReturnValue(messages);
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
|
||||
@@ -79,10 +73,7 @@ describe('DetailedMessagesDisplay', () => {
|
||||
const messages: ConsoleMessageItem[] = [
|
||||
{ type: 'error', content: 'Error message', count: 1 },
|
||||
];
|
||||
vi.mocked(useConsoleMessages).mockReturnValue({
|
||||
consoleMessages: messages,
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
vi.mocked(useConsoleMessages).mockReturnValue(messages);
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
|
||||
@@ -98,10 +89,7 @@ describe('DetailedMessagesDisplay', () => {
|
||||
const messages: ConsoleMessageItem[] = [
|
||||
{ type: 'error', content: 'Error message', count: 1 },
|
||||
];
|
||||
vi.mocked(useConsoleMessages).mockReturnValue({
|
||||
consoleMessages: messages,
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
vi.mocked(useConsoleMessages).mockReturnValue(messages);
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
|
||||
@@ -117,10 +105,7 @@ describe('DetailedMessagesDisplay', () => {
|
||||
const messages: ConsoleMessageItem[] = [
|
||||
{ type: 'log', content: 'Repeated message', count: 5 },
|
||||
];
|
||||
vi.mocked(useConsoleMessages).mockReturnValue({
|
||||
consoleMessages: messages,
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
vi.mocked(useConsoleMessages).mockReturnValue(messages);
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={10} width={80} hasFocus={false} />,
|
||||
|
||||
@@ -29,7 +29,7 @@ export const DetailedMessagesDisplay: React.FC<
|
||||
> = ({ maxHeight, width, hasFocus }) => {
|
||||
const scrollableListRef = useRef<ScrollableListRef<ConsoleMessageItem>>(null);
|
||||
|
||||
const { consoleMessages } = useConsoleMessages();
|
||||
const consoleMessages = useConsoleMessages();
|
||||
const config = useConfig();
|
||||
|
||||
const messages = useMemo(() => {
|
||||
|
||||
@@ -434,6 +434,7 @@ describe('<Footer />', () => {
|
||||
|
||||
it('renders footer with all optional sections hidden (minimal footer)', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
@@ -551,6 +552,7 @@ describe('<Footer />', () => {
|
||||
describe('Footer Token Formatting', () => {
|
||||
const renderWithTokens = async (tokens: number) => {
|
||||
const result = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: {
|
||||
@@ -734,6 +736,7 @@ describe('<Footer />', () => {
|
||||
|
||||
it('handles empty items array', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
|
||||
@@ -175,18 +175,12 @@ interface FooterColumn {
|
||||
isHighPriority: boolean;
|
||||
}
|
||||
|
||||
export const Footer: React.FC<{ copyModeEnabled?: boolean }> = ({
|
||||
copyModeEnabled = false,
|
||||
}) => {
|
||||
export const Footer: React.FC = () => {
|
||||
const uiState = useUIState();
|
||||
const config = useConfig();
|
||||
const settings = useSettings();
|
||||
const { vimEnabled, vimMode } = useVimMode();
|
||||
|
||||
if (copyModeEnabled) {
|
||||
return <Box height={1} />;
|
||||
}
|
||||
|
||||
const {
|
||||
model,
|
||||
targetDir,
|
||||
@@ -359,17 +353,7 @@ export const Footer: React.FC<{ copyModeEnabled?: boolean }> = ({
|
||||
break;
|
||||
}
|
||||
case 'memory-usage': {
|
||||
addCol(
|
||||
id,
|
||||
header,
|
||||
() => (
|
||||
<MemoryUsageDisplay
|
||||
color={itemColor}
|
||||
isActive={!uiState.copyModeEnabled}
|
||||
/>
|
||||
),
|
||||
10,
|
||||
);
|
||||
addCol(id, header, () => <MemoryUsageDisplay color={itemColor} />, 10);
|
||||
break;
|
||||
}
|
||||
case 'session-id': {
|
||||
|
||||
@@ -2137,85 +2137,67 @@ describe('InputPrompt', () => {
|
||||
name: 'mid-word',
|
||||
text: 'hello world',
|
||||
visualCursor: [0, 3],
|
||||
expected: `hel${chalk.inverse('l')}o world`,
|
||||
},
|
||||
{
|
||||
name: 'at the beginning of the line',
|
||||
text: 'hello',
|
||||
visualCursor: [0, 0],
|
||||
expected: `${chalk.inverse('h')}ello`,
|
||||
},
|
||||
{
|
||||
name: 'at the end of the line',
|
||||
text: 'hello',
|
||||
visualCursor: [0, 5],
|
||||
expected: `hello${chalk.inverse(' ')}`,
|
||||
},
|
||||
{
|
||||
name: 'on a highlighted token',
|
||||
text: 'run @path/to/file',
|
||||
visualCursor: [0, 9],
|
||||
expected: `@path/${chalk.inverse('t')}o/file`,
|
||||
},
|
||||
{
|
||||
name: 'for multi-byte unicode characters',
|
||||
text: 'hello 👍 world',
|
||||
visualCursor: [0, 6],
|
||||
expected: `hello ${chalk.inverse('👍')} world`,
|
||||
},
|
||||
{
|
||||
name: 'after multi-byte unicode characters',
|
||||
text: '👍A',
|
||||
visualCursor: [0, 1],
|
||||
expected: `👍${chalk.inverse('A')}`,
|
||||
},
|
||||
{
|
||||
name: 'at the end of a line with unicode characters',
|
||||
text: 'hello 👍',
|
||||
visualCursor: [0, 8],
|
||||
expected: `hello 👍`, // skip checking inverse ansi due to ink truncation bug
|
||||
},
|
||||
{
|
||||
name: 'at the end of a short line with unicode characters',
|
||||
text: '👍',
|
||||
visualCursor: [0, 1],
|
||||
expected: `👍${chalk.inverse(' ')}`,
|
||||
},
|
||||
{
|
||||
name: 'on an empty line',
|
||||
text: '',
|
||||
visualCursor: [0, 0],
|
||||
expected: chalk.inverse(' '),
|
||||
},
|
||||
{
|
||||
name: 'on a space between words',
|
||||
text: 'hello world',
|
||||
visualCursor: [0, 5],
|
||||
expected: `hello${chalk.inverse(' ')}world`,
|
||||
},
|
||||
])(
|
||||
'should display cursor correctly $name',
|
||||
async ({ name, text, visualCursor, expected }) => {
|
||||
async ({ text, visualCursor }) => {
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = [text];
|
||||
mockBuffer.viewportVisualLines = [text];
|
||||
mockBuffer.visualCursor = visualCursor as [number, number];
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const { stdout, unmount } = await renderWithProviders(
|
||||
const renderResult = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrameRaw();
|
||||
expect(stripAnsi(frame)).toContain(stripAnsi(expected));
|
||||
if (
|
||||
name !== 'at the end of a line with unicode characters' &&
|
||||
name !== 'on a highlighted token'
|
||||
) {
|
||||
expect(frame).toContain('\u001b[7m');
|
||||
}
|
||||
});
|
||||
unmount();
|
||||
await renderResult.waitUntilReady();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -2231,7 +2213,6 @@ describe('InputPrompt', () => {
|
||||
[1, 0],
|
||||
[2, 0],
|
||||
],
|
||||
expected: `sec${chalk.inverse('o')}nd line`,
|
||||
},
|
||||
{
|
||||
name: 'at the beginning of a line',
|
||||
@@ -2241,7 +2222,6 @@ describe('InputPrompt', () => {
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
],
|
||||
expected: `${chalk.inverse('s')}econd line`,
|
||||
},
|
||||
{
|
||||
name: 'at the end of a line',
|
||||
@@ -2251,11 +2231,10 @@ describe('InputPrompt', () => {
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
],
|
||||
expected: `first line${chalk.inverse(' ')}`,
|
||||
},
|
||||
])(
|
||||
'should display cursor correctly $name in a multiline block',
|
||||
async ({ name, text, visualCursor, expected, visualToLogicalMap }) => {
|
||||
async ({ text, visualCursor, visualToLogicalMap }) => {
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = text.split('\n');
|
||||
mockBuffer.viewportVisualLines = text.split('\n');
|
||||
@@ -2265,20 +2244,12 @@ describe('InputPrompt', () => {
|
||||
>;
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const { stdout, unmount } = await renderWithProviders(
|
||||
const renderResult = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrameRaw();
|
||||
expect(stripAnsi(frame)).toContain(stripAnsi(expected));
|
||||
if (
|
||||
name !== 'at the end of a line with unicode characters' &&
|
||||
name !== 'on a highlighted token'
|
||||
) {
|
||||
expect(frame).toContain('\u001b[7m');
|
||||
}
|
||||
});
|
||||
unmount();
|
||||
await renderResult.waitUntilReady();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2295,18 +2266,12 @@ describe('InputPrompt', () => {
|
||||
];
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const { stdout, unmount } = await renderWithProviders(
|
||||
const renderResult = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrameRaw();
|
||||
const lines = frame.split('\n');
|
||||
// The line with the cursor should just be an inverted space inside the box border
|
||||
expect(
|
||||
lines.find((l) => l.includes(chalk.inverse(' '))),
|
||||
).not.toBeUndefined();
|
||||
});
|
||||
unmount();
|
||||
await renderResult.waitUntilReady();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2327,22 +2292,14 @@ describe('InputPrompt', () => {
|
||||
];
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const { stdout, unmount } = await renderWithProviders(
|
||||
const renderResult = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrameRaw();
|
||||
// Check that all lines, including the empty one, are rendered.
|
||||
// This implicitly tests that the Box wrapper provides height for the empty line.
|
||||
expect(frame).toContain('hello');
|
||||
expect(frame).toContain('world');
|
||||
expect(frame).toContain(chalk.inverse(' '));
|
||||
|
||||
const outputLines = frame.trim().split('\n');
|
||||
// The number of lines should be 2 for the border plus 3 for the content.
|
||||
expect(outputLines.length).toBe(5);
|
||||
});
|
||||
unmount();
|
||||
await renderResult.waitUntilReady();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
|
||||
renderResult.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3955,14 +3912,12 @@ describe('InputPrompt', () => {
|
||||
it('should not show inverted cursor when shell is focused', async () => {
|
||||
props.isEmbeddedShellFocused = true;
|
||||
props.focus = false;
|
||||
const { stdout, unmount } = await renderWithProviders(
|
||||
const renderResult = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`);
|
||||
});
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
await renderResult.waitUntilReady();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -119,7 +119,6 @@ export interface InputPromptProps {
|
||||
popAllMessages?: () => string | undefined;
|
||||
suggestionsPosition?: 'above' | 'below';
|
||||
setBannerVisible: (visible: boolean) => void;
|
||||
copyModeEnabled?: boolean;
|
||||
}
|
||||
|
||||
// The input content, input container, and input suggestions list may have different widths
|
||||
@@ -213,7 +212,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
popAllMessages,
|
||||
suggestionsPosition = 'below',
|
||||
setBannerVisible,
|
||||
copyModeEnabled = false,
|
||||
}) => {
|
||||
const isHelpDismissKey = useIsHelpDismissKey();
|
||||
const keyMatchers = useKeyMatchers();
|
||||
@@ -333,8 +331,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
isShellSuggestionsVisible,
|
||||
} = completion;
|
||||
|
||||
const showCursor =
|
||||
focus && isShellFocused && !isEmbeddedShellFocused && !copyModeEnabled;
|
||||
const showCursor = focus && isShellFocused && !isEmbeddedShellFocused;
|
||||
|
||||
// Notify parent component about escape prompt state changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -21,7 +21,10 @@ import {
|
||||
type UIState,
|
||||
} from '../contexts/UIStateContext.js';
|
||||
import { type IndividualToolCallDisplay } from '../types.js';
|
||||
import { type ConfirmingToolState } from '../hooks/useConfirmingTool.js';
|
||||
import {
|
||||
type ConfirmingToolState,
|
||||
useConfirmingTool,
|
||||
} from '../hooks/useConfirmingTool.js';
|
||||
|
||||
// Mock dependencies
|
||||
const mockUseSettings = vi.fn().mockReturnValue({
|
||||
@@ -548,10 +551,11 @@ describe('MainContent', () => {
|
||||
config: makeFakeConfig({ useAlternateBuffer: false }),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('codebase_investigator');
|
||||
});
|
||||
|
||||
expect(output).toContain('codebase_investigator');
|
||||
expect(output).toMatchSnapshot();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -599,13 +603,16 @@ describe('MainContent', () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(<MainContent />, {
|
||||
uiState: uiState as Partial<UIState>,
|
||||
});
|
||||
const output = lastFrame();
|
||||
// Verify Part 1 and Part 2 are rendered.
|
||||
expect(output).toContain('Part 1');
|
||||
expect(output).toContain('Part 2');
|
||||
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
// Verify Part 1 and Part 2 are rendered.
|
||||
expect(output).toContain('Part 1');
|
||||
expect(output).toContain('Part 2');
|
||||
});
|
||||
|
||||
// The snapshot will be the best way to verify there is no gap (empty line) between them.
|
||||
expect(output).toMatchSnapshot();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -650,7 +657,6 @@ describe('MainContent', () => {
|
||||
};
|
||||
|
||||
// We need to mock useConfirmingTool to return our confirmingTool
|
||||
const { useConfirmingTool } = await import('../hooks/useConfirmingTool.js');
|
||||
vi.mocked(useConfirmingTool).mockReturnValue(
|
||||
confirmingTool as unknown as ConfirmingToolState,
|
||||
);
|
||||
@@ -667,15 +673,16 @@ describe('MainContent', () => {
|
||||
config: makeFakeConfig({ useAlternateBuffer: false }),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
|
||||
// The output should NOT contain 'Hidden content'
|
||||
expect(output).not.toContain('Hidden content');
|
||||
// The output should contain the confirmation header
|
||||
expect(output).toContain('Ready to start implementation?');
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
// The output should NOT contain 'Hidden content'
|
||||
expect(output).not.toContain('Hidden content');
|
||||
// The output should contain the confirmation header
|
||||
expect(output).toContain('Ready to start implementation?');
|
||||
});
|
||||
|
||||
// Snapshot will reveal if there are extra blank lines
|
||||
expect(output).toMatchSnapshot();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -708,9 +715,12 @@ describe('MainContent', () => {
|
||||
config: makeFakeConfig({ useAlternateBuffer: false }),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Apply plan');
|
||||
});
|
||||
|
||||
// This snapshot will show no spurious line because the group is now correctly suppressed.
|
||||
expect(output).toMatchSnapshot();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
|
||||
@@ -11,18 +11,13 @@ import { theme } from '../semantic-colors.js';
|
||||
import process from 'node:process';
|
||||
import { formatBytes } from '../utils/formatters.js';
|
||||
|
||||
export const MemoryUsageDisplay: React.FC<{
|
||||
color?: string;
|
||||
isActive?: boolean;
|
||||
}> = ({ color = theme.text.primary, isActive = true }) => {
|
||||
export const MemoryUsageDisplay: React.FC<{ color?: string }> = ({
|
||||
color = theme.text.primary,
|
||||
}) => {
|
||||
const [memoryUsage, setMemoryUsage] = useState<string>('');
|
||||
const [memoryUsageColor, setMemoryUsageColor] = useState<string>(color);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateMemory = () => {
|
||||
const usage = process.memoryUsage().rss;
|
||||
setMemoryUsage(formatBytes(usage));
|
||||
@@ -30,11 +25,10 @@ export const MemoryUsageDisplay: React.FC<{
|
||||
usage >= 2 * 1024 * 1024 * 1024 ? theme.status.error : color,
|
||||
);
|
||||
};
|
||||
|
||||
const intervalId = setInterval(updateMemory, 2000);
|
||||
updateMemory(); // Initial update
|
||||
return () => clearInterval(intervalId);
|
||||
}, [color, isActive]);
|
||||
}, [color]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
|
||||
AuthType,
|
||||
UserTierId,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Config, ModelSlashCommandEvent } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -53,9 +52,9 @@ describe('<ModelDialog />', () => {
|
||||
const mockOnClose = vi.fn();
|
||||
const mockGetHasAccessToPreviewModel = vi.fn();
|
||||
const mockGetGemini31LaunchedSync = vi.fn();
|
||||
const mockGetGemini31FlashLiteLaunchedSync = vi.fn();
|
||||
const mockGetProModelNoAccess = vi.fn();
|
||||
const mockGetProModelNoAccessSync = vi.fn();
|
||||
const mockGetUserTier = vi.fn();
|
||||
|
||||
interface MockConfig extends Partial<Config> {
|
||||
setModel: (model: string, isTemporary?: boolean) => void;
|
||||
@@ -63,9 +62,9 @@ describe('<ModelDialog />', () => {
|
||||
getHasAccessToPreviewModel: () => boolean;
|
||||
getIdeMode: () => boolean;
|
||||
getGemini31LaunchedSync: () => boolean;
|
||||
getGemini31FlashLiteLaunchedSync: () => boolean;
|
||||
getProModelNoAccess: () => Promise<boolean>;
|
||||
getProModelNoAccessSync: () => boolean;
|
||||
getUserTier: () => UserTierId | undefined;
|
||||
}
|
||||
|
||||
const mockConfig: MockConfig = {
|
||||
@@ -74,9 +73,9 @@ describe('<ModelDialog />', () => {
|
||||
getHasAccessToPreviewModel: mockGetHasAccessToPreviewModel,
|
||||
getIdeMode: () => false,
|
||||
getGemini31LaunchedSync: mockGetGemini31LaunchedSync,
|
||||
getGemini31FlashLiteLaunchedSync: mockGetGemini31FlashLiteLaunchedSync,
|
||||
getProModelNoAccess: mockGetProModelNoAccess,
|
||||
getProModelNoAccessSync: mockGetProModelNoAccessSync,
|
||||
getUserTier: mockGetUserTier,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -84,9 +83,9 @@ describe('<ModelDialog />', () => {
|
||||
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(false);
|
||||
mockGetGemini31LaunchedSync.mockReturnValue(false);
|
||||
mockGetGemini31FlashLiteLaunchedSync.mockReturnValue(false);
|
||||
mockGetProModelNoAccess.mockResolvedValue(false);
|
||||
mockGetProModelNoAccessSync.mockReturnValue(false);
|
||||
mockGetUserTier.mockReturnValue(UserTierId.STANDARD);
|
||||
|
||||
// Default implementation for getDisplayString
|
||||
mockGetDisplayString.mockImplementation((val: string) => {
|
||||
@@ -131,7 +130,7 @@ describe('<ModelDialog />', () => {
|
||||
mockGetProModelNoAccessSync.mockReturnValue(true);
|
||||
mockGetProModelNoAccess.mockResolvedValue(true);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||
mockGetUserTier.mockReturnValue(UserTierId.FREE);
|
||||
mockGetGemini31FlashLiteLaunchedSync.mockReturnValue(true);
|
||||
mockGetDisplayString.mockImplementation((val: string) => val);
|
||||
|
||||
const { lastFrame, unmount } = await renderComponent();
|
||||
@@ -437,33 +436,11 @@ describe('<ModelDialog />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides Flash Lite Preview model for users with pro access', async () => {
|
||||
it('shows Flash Lite Preview model regardless of tier when flag is enabled', async () => {
|
||||
mockGetProModelNoAccessSync.mockReturnValue(false);
|
||||
mockGetProModelNoAccess.mockResolvedValue(false);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||
const { lastFrame, stdin, waitUntilReady, unmount } =
|
||||
await renderComponent();
|
||||
|
||||
// Go to manual view
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Manual
|
||||
});
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows Flash Lite Preview model for free tier users', async () => {
|
||||
mockGetProModelNoAccessSync.mockReturnValue(false);
|
||||
mockGetProModelNoAccess.mockResolvedValue(false);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||
mockGetUserTier.mockReturnValue(UserTierId.FREE);
|
||||
mockGetGemini31FlashLiteLaunchedSync.mockReturnValue(true);
|
||||
const { lastFrame, stdin, waitUntilReady, unmount } =
|
||||
await renderComponent();
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
AuthType,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
isProModel,
|
||||
UserTierId,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
@@ -63,6 +62,8 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
|
||||
const shouldShowPreviewModels = config?.getHasAccessToPreviewModel();
|
||||
const useGemini31 = config?.getGemini31LaunchedSync?.() ?? false;
|
||||
const useGemini31FlashLite =
|
||||
config?.getGemini31FlashLiteLaunchedSync?.() ?? false;
|
||||
const selectedAuthType = settings.merged.security.auth.selectedType;
|
||||
const useCustomToolModel =
|
||||
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
|
||||
@@ -86,6 +87,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
];
|
||||
if (manualModels.includes(preferredModel)) {
|
||||
@@ -187,7 +189,6 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
}, [config, shouldShowPreviewModels, manualModelSelected, useGemini31]);
|
||||
|
||||
const manualOptions = useMemo(() => {
|
||||
const isFreeTier = config?.getUserTier() === UserTierId.FREE;
|
||||
// --- DYNAMIC PATH ---
|
||||
if (
|
||||
config?.getExperimentalDynamicModelConfiguration?.() === true &&
|
||||
@@ -204,13 +205,13 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
if (m.tier === 'auto') return false;
|
||||
// Pro models are shown for users with pro access
|
||||
if (!hasAccessToProModel && m.tier === 'pro') return false;
|
||||
// 3.1 Preview Flash-lite is only available on free tier
|
||||
if (m.tier === 'flash-lite' && m.isPreview && !isFreeTier)
|
||||
return false;
|
||||
|
||||
// Flag Guard: Versioned models only show if their flag is active.
|
||||
if (id === PREVIEW_GEMINI_3_1_MODEL && !useGemini31) return false;
|
||||
if (id === PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL && !useGemini31)
|
||||
if (
|
||||
id === PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL &&
|
||||
!useGemini31FlashLite
|
||||
)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
@@ -218,11 +219,13 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
.map(([id, m]) => {
|
||||
const resolvedId = config.modelConfigService.resolveModelId(id, {
|
||||
useGemini3_1: useGemini31,
|
||||
useGemini3_1FlashLite: useGemini31FlashLite,
|
||||
useCustomTools: useCustomToolModel,
|
||||
});
|
||||
// Title ID is the resolved ID without custom tools flag
|
||||
const titleId = config.modelConfigService.resolveModelId(id, {
|
||||
useGemini3_1: useGemini31,
|
||||
useGemini3_1FlashLite: useGemini31FlashLite,
|
||||
});
|
||||
return {
|
||||
value: resolvedId,
|
||||
@@ -284,7 +287,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
if (isFreeTier) {
|
||||
if (useGemini31FlashLite) {
|
||||
previewOptions.push({
|
||||
value: PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
|
||||
title: getDisplayString(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL),
|
||||
@@ -304,6 +307,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
}, [
|
||||
shouldShowPreviewModels,
|
||||
useGemini31,
|
||||
useGemini31FlashLite,
|
||||
useCustomToolModel,
|
||||
hasAccessToProModel,
|
||||
config,
|
||||
|
||||
@@ -92,6 +92,7 @@ const buildModelRows = (
|
||||
config: Config,
|
||||
quotas?: RetrieveUserQuotaResponse,
|
||||
useGemini3_1 = false,
|
||||
useGemini3_1FlashLite = false,
|
||||
useCustomToolModel = false,
|
||||
) => {
|
||||
const getBaseModelName = (name: string) => name.replace('-001', '');
|
||||
@@ -124,7 +125,12 @@ const buildModelRows = (
|
||||
?.filter(
|
||||
(b) =>
|
||||
b.modelId &&
|
||||
isActiveModel(b.modelId, useGemini3_1, useCustomToolModel) &&
|
||||
isActiveModel(
|
||||
b.modelId,
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
useCustomToolModel,
|
||||
) &&
|
||||
!usedModelNames.has(getDisplayString(b.modelId, config)),
|
||||
)
|
||||
.map((bucket) => ({
|
||||
@@ -152,6 +158,7 @@ const ModelUsageTable: React.FC<{
|
||||
pooledLimit?: number;
|
||||
pooledResetTime?: string;
|
||||
useGemini3_1?: boolean;
|
||||
useGemini3_1FlashLite?: boolean;
|
||||
useCustomToolModel?: boolean;
|
||||
}> = ({
|
||||
models,
|
||||
@@ -164,6 +171,7 @@ const ModelUsageTable: React.FC<{
|
||||
pooledLimit,
|
||||
pooledResetTime,
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
useCustomToolModel,
|
||||
}) => {
|
||||
const { stdout } = useStdout();
|
||||
@@ -173,6 +181,7 @@ const ModelUsageTable: React.FC<{
|
||||
config,
|
||||
quotas,
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
useCustomToolModel,
|
||||
);
|
||||
|
||||
@@ -541,6 +550,8 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
const settings = useSettings();
|
||||
const config = useConfig();
|
||||
const useGemini3_1 = config.getGemini31LaunchedSync?.() ?? false;
|
||||
const useGemini3_1FlashLite =
|
||||
config.getGemini31FlashLiteLaunchedSync?.() ?? false;
|
||||
const useCustomToolModel =
|
||||
useGemini3_1 &&
|
||||
config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI;
|
||||
@@ -697,6 +708,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
pooledLimit={pooledLimit}
|
||||
pooledResetTime={pooledResetTime}
|
||||
useGemini3_1={useGemini3_1}
|
||||
useGemini3_1FlashLite={useGemini3_1FlashLite}
|
||||
useCustomToolModel={useCustomToolModel}
|
||||
/>
|
||||
{renderFooter()}
|
||||
|
||||
@@ -1,424 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { Box, Text, ResizeObserver, type DOMElement } from 'ink';
|
||||
import {
|
||||
isUserVisibleHook,
|
||||
type ThoughtSummary,
|
||||
} from '@google/gemini-cli-core';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { type ActiveHook } from '../types.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { GENERIC_WORKING_LABEL } from '../textConstants.js';
|
||||
import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js';
|
||||
import { LoadingIndicator } from './LoadingIndicator.js';
|
||||
import { StatusDisplay } from './StatusDisplay.js';
|
||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||
import { HorizontalLine } from './shared/HorizontalLine.js';
|
||||
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
|
||||
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
||||
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
|
||||
import { useComposerStatus } from '../hooks/useComposerStatus.js';
|
||||
|
||||
/**
|
||||
* Layout constants to prevent magic numbers.
|
||||
*/
|
||||
const LAYOUT = {
|
||||
STATUS_MIN_HEIGHT: 1,
|
||||
TIP_LEFT_MARGIN: 2,
|
||||
TIP_RIGHT_MARGIN_NARROW: 0,
|
||||
TIP_RIGHT_MARGIN_WIDE: 1,
|
||||
INDICATOR_LEFT_MARGIN: 1,
|
||||
CONTEXT_DISPLAY_TOP_MARGIN_NARROW: 1,
|
||||
CONTEXT_DISPLAY_LEFT_MARGIN_NARROW: 1,
|
||||
CONTEXT_DISPLAY_LEFT_MARGIN_WIDE: 0,
|
||||
COLLISION_GAP: 10,
|
||||
};
|
||||
|
||||
interface StatusRowProps {
|
||||
showUiDetails: boolean;
|
||||
isNarrow: boolean;
|
||||
terminalWidth: number;
|
||||
hideContextSummary: boolean;
|
||||
hideUiDetailsForSuggestions: boolean;
|
||||
hasPendingActionRequired: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the loading or hook execution status.
|
||||
*/
|
||||
export const StatusNode: React.FC<{
|
||||
showTips: boolean;
|
||||
showWit: boolean;
|
||||
thought: ThoughtSummary | null;
|
||||
elapsedTime: number;
|
||||
currentWittyPhrase: string | undefined;
|
||||
activeHooks: ActiveHook[];
|
||||
showLoadingIndicator: boolean;
|
||||
errorVerbosity: 'low' | 'full' | undefined;
|
||||
onResize?: (width: number) => void;
|
||||
}> = ({
|
||||
showTips,
|
||||
showWit,
|
||||
thought,
|
||||
elapsedTime,
|
||||
currentWittyPhrase,
|
||||
activeHooks,
|
||||
showLoadingIndicator,
|
||||
errorVerbosity,
|
||||
onResize,
|
||||
}) => {
|
||||
const observerRef = useRef<ResizeObserver | null>(null);
|
||||
|
||||
const onRefChange = useCallback(
|
||||
(node: DOMElement | null) => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
observerRef.current = null;
|
||||
}
|
||||
|
||||
if (node && onResize) {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
onResize(Math.round(entry.contentRect.width));
|
||||
}
|
||||
});
|
||||
observer.observe(node);
|
||||
observerRef.current = observer;
|
||||
}
|
||||
},
|
||||
[onResize],
|
||||
);
|
||||
|
||||
if (activeHooks.length === 0 && !showLoadingIndicator) return null;
|
||||
|
||||
let currentLoadingPhrase: string | undefined = undefined;
|
||||
let currentThought: ThoughtSummary | null = null;
|
||||
|
||||
if (activeHooks.length > 0) {
|
||||
const userVisibleHooks = activeHooks.filter((h) =>
|
||||
isUserVisibleHook(h.source),
|
||||
);
|
||||
|
||||
if (userVisibleHooks.length > 0) {
|
||||
const label =
|
||||
userVisibleHooks.length > 1 ? 'Executing Hooks' : 'Executing Hook';
|
||||
const displayNames = userVisibleHooks.map((h) => {
|
||||
let name = stripAnsi(h.name);
|
||||
if (h.index && h.total && h.total > 1) {
|
||||
name += ` (${h.index}/${h.total})`;
|
||||
}
|
||||
return name;
|
||||
});
|
||||
currentLoadingPhrase = `${label}: ${displayNames.join(', ')}`;
|
||||
} else {
|
||||
currentLoadingPhrase = GENERIC_WORKING_LABEL;
|
||||
}
|
||||
} else {
|
||||
// Sanitize thought subject to prevent terminal injection
|
||||
currentThought = thought
|
||||
? { ...thought, subject: stripAnsi(thought.subject) }
|
||||
: null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box ref={onRefChange}>
|
||||
<LoadingIndicator
|
||||
inline
|
||||
showTips={showTips}
|
||||
showWit={showWit}
|
||||
errorVerbosity={errorVerbosity}
|
||||
thought={currentThought}
|
||||
currentLoadingPhrase={currentLoadingPhrase}
|
||||
elapsedTime={elapsedTime}
|
||||
forceRealStatusOnly={false}
|
||||
wittyPhrase={currentWittyPhrase}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
showUiDetails,
|
||||
isNarrow,
|
||||
terminalWidth,
|
||||
hideContextSummary,
|
||||
hideUiDetailsForSuggestions,
|
||||
hasPendingActionRequired,
|
||||
}) => {
|
||||
const uiState = useUIState();
|
||||
const settings = useSettings();
|
||||
const {
|
||||
isInteractiveShellWaiting,
|
||||
showLoadingIndicator,
|
||||
showTips,
|
||||
showWit,
|
||||
modeContentObj,
|
||||
showMinimalContext,
|
||||
} = useComposerStatus();
|
||||
|
||||
const [statusWidth, setStatusWidth] = useState(0);
|
||||
const [tipWidth, setTipWidth] = useState(0);
|
||||
const tipObserverRef = useRef<ResizeObserver | null>(null);
|
||||
|
||||
const onTipRefChange = useCallback((node: DOMElement | null) => {
|
||||
if (tipObserverRef.current) {
|
||||
tipObserverRef.current.disconnect();
|
||||
tipObserverRef.current = null;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
setTipWidth(Math.round(entry.contentRect.width));
|
||||
}
|
||||
});
|
||||
observer.observe(node);
|
||||
tipObserverRef.current = observer;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const tipContentStr = (() => {
|
||||
// 1. Proactive Tip (Priority)
|
||||
if (
|
||||
showTips &&
|
||||
uiState.currentTip &&
|
||||
!(
|
||||
isInteractiveShellWaiting &&
|
||||
uiState.currentTip === INTERACTIVE_SHELL_WAITING_PHRASE
|
||||
)
|
||||
) {
|
||||
return uiState.currentTip;
|
||||
}
|
||||
|
||||
// 2. Shortcut Hint (Fallback)
|
||||
if (
|
||||
settings.merged.ui.showShortcutsHint &&
|
||||
!hideUiDetailsForSuggestions &&
|
||||
!hasPendingActionRequired &&
|
||||
uiState.buffer.text.length === 0
|
||||
) {
|
||||
return showUiDetails ? '? for shortcuts' : 'press tab twice for more';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
// Collision detection using measured widths
|
||||
const willCollideTip =
|
||||
statusWidth + tipWidth + LAYOUT.COLLISION_GAP > terminalWidth;
|
||||
|
||||
const showTipLine = Boolean(
|
||||
!hasPendingActionRequired && tipContentStr && !willCollideTip && !isNarrow,
|
||||
);
|
||||
|
||||
const showRow1Minimal =
|
||||
showLoadingIndicator || uiState.activeHooks.length > 0 || showTipLine;
|
||||
const showRow2Minimal =
|
||||
(Boolean(modeContentObj) && !hideUiDetailsForSuggestions) ||
|
||||
showMinimalContext;
|
||||
|
||||
const showRow1 = showUiDetails || showRow1Minimal;
|
||||
const showRow2 = showUiDetails || showRow2Minimal;
|
||||
|
||||
const statusNode = (
|
||||
<StatusNode
|
||||
showTips={showTips}
|
||||
showWit={showWit}
|
||||
thought={uiState.thought}
|
||||
elapsedTime={uiState.elapsedTime}
|
||||
currentWittyPhrase={uiState.currentWittyPhrase}
|
||||
activeHooks={uiState.activeHooks}
|
||||
showLoadingIndicator={showLoadingIndicator}
|
||||
errorVerbosity={
|
||||
settings.merged.ui.errorVerbosity as 'low' | 'full' | undefined
|
||||
}
|
||||
onResize={setStatusWidth}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderTipNode = () => {
|
||||
if (!tipContentStr) return null;
|
||||
|
||||
const isShortcutHint =
|
||||
tipContentStr === '? for shortcuts' ||
|
||||
tipContentStr === 'press tab twice for more';
|
||||
const color =
|
||||
isShortcutHint && uiState.shortcutsHelpVisible
|
||||
? theme.text.accent
|
||||
: theme.text.secondary;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" justifyContent="flex-end" ref={onTipRefChange}>
|
||||
<Text
|
||||
color={color}
|
||||
wrap="truncate-end"
|
||||
italic={
|
||||
!isShortcutHint && tipContentStr === uiState.currentWittyPhrase
|
||||
}
|
||||
>
|
||||
{tipContentStr === uiState.currentTip
|
||||
? `Tip: ${tipContentStr}`
|
||||
: tipContentStr}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
if (!showUiDetails && !showRow1Minimal && !showRow2Minimal) {
|
||||
return <Box height={LAYOUT.STATUS_MIN_HEIGHT} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width="100%">
|
||||
{/* Row 1: Status & Tips */}
|
||||
{showRow1 && (
|
||||
<Box
|
||||
width="100%"
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
minHeight={LAYOUT.STATUS_MIN_HEIGHT}
|
||||
>
|
||||
<Box flexDirection="row" flexGrow={1} flexShrink={1}>
|
||||
{!showUiDetails && showRow1Minimal ? (
|
||||
<Box flexDirection="row" columnGap={1}>
|
||||
{statusNode}
|
||||
{!showUiDetails && showRow2Minimal && modeContentObj && (
|
||||
<Box>
|
||||
<Text color={modeContentObj.color}>
|
||||
● {modeContentObj.text}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : isInteractiveShellWaiting ? (
|
||||
<Box width="100%" marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}>
|
||||
<Text color={theme.status.warning}>
|
||||
! Shell awaiting input (Tab to focus)
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
flexGrow={1}
|
||||
flexShrink={0}
|
||||
marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}
|
||||
>
|
||||
{statusNode}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
flexShrink={0}
|
||||
marginLeft={LAYOUT.TIP_LEFT_MARGIN}
|
||||
marginRight={
|
||||
isNarrow
|
||||
? LAYOUT.TIP_RIGHT_MARGIN_NARROW
|
||||
: LAYOUT.TIP_RIGHT_MARGIN_WIDE
|
||||
}
|
||||
>
|
||||
{/*
|
||||
We always render the tip node so it can be measured by ResizeObserver,
|
||||
but we control its visibility based on the collision detection.
|
||||
*/}
|
||||
<Box display={showTipLine ? 'flex' : 'none'}>
|
||||
{!isNarrow && tipContentStr && renderTipNode()}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Internal Separator */}
|
||||
{showRow1 &&
|
||||
showRow2 &&
|
||||
(showUiDetails || (showRow1Minimal && showRow2Minimal)) && (
|
||||
<Box width="100%">
|
||||
<HorizontalLine dim />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Row 2: Modes & Context */}
|
||||
{showRow2 && (
|
||||
<Box
|
||||
width="100%"
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Box
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}
|
||||
>
|
||||
{showUiDetails ? (
|
||||
<>
|
||||
{!hideUiDetailsForSuggestions && !uiState.shellModeActive && (
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={uiState.showApprovalModeIndicator}
|
||||
allowPlanMode={uiState.allowPlanMode}
|
||||
/>
|
||||
)}
|
||||
{uiState.shellModeActive && (
|
||||
<Box marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}>
|
||||
<ShellModeIndicator />
|
||||
</Box>
|
||||
)}
|
||||
{!uiState.renderMarkdown && (
|
||||
<Box marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}>
|
||||
<RawMarkdownIndicator />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
showRow2Minimal &&
|
||||
modeContentObj && (
|
||||
<Text color={modeContentObj.color}>
|
||||
● {modeContentObj.text}
|
||||
</Text>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
marginTop={isNarrow ? LAYOUT.CONTEXT_DISPLAY_TOP_MARGIN_NARROW : 0}
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
marginLeft={
|
||||
isNarrow
|
||||
? LAYOUT.CONTEXT_DISPLAY_LEFT_MARGIN_NARROW
|
||||
: LAYOUT.CONTEXT_DISPLAY_LEFT_MARGIN_WIDE
|
||||
}
|
||||
>
|
||||
{(showUiDetails || showMinimalContext) && (
|
||||
<StatusDisplay hideContextSummary={hideContextSummary} />
|
||||
)}
|
||||
{showMinimalContext && !showUiDetails && (
|
||||
<Box marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}>
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={uiState.sessionStats.lastPromptTokenCount}
|
||||
model={
|
||||
typeof uiState.currentModel === 'string'
|
||||
? uiState.currentModel
|
||||
: undefined
|
||||
}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="88" viewBox="0 0 920 88">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="88" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">first line</text>
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="34" width="9" height="17" fill="#ffffff" />
|
||||
<text x="36" y="36" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">s</text>
|
||||
<text x="45" y="36" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">econd line</text>
|
||||
<text x="891" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="88" viewBox="0 0 920 88">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="88" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">first line</text>
|
||||
<rect x="126" y="17" width="9" height="17" fill="#ffffff" />
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs">second line</text>
|
||||
<text x="891" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="105" viewBox="0 0 920 105">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="105" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">first line</text>
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="36" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">sec</text>
|
||||
<rect x="63" y="34" width="9" height="17" fill="#ffffff" />
|
||||
<text x="63" y="36" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">o</text>
|
||||
<text x="72" y="36" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">nd line</text>
|
||||
<text x="891" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="53" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">third line</text>
|
||||
<text x="891" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="105" viewBox="0 0 920 105">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="105" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">first line</text>
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="34" width="9" height="17" fill="#ffffff" />
|
||||
<text x="891" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="53" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">third line</text>
|
||||
<text x="891" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="9" lengthAdjust="spacingAndGlyphs">👍</text>
|
||||
<rect x="45" y="17" width="9" height="17" fill="#ffffff" />
|
||||
<text x="45" y="19" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">A</text>
|
||||
<text x="882" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<rect x="36" y="17" width="9" height="17" fill="#ffffff" />
|
||||
<text x="36" y="19" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">h</text>
|
||||
<text x="45" y="19" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">ello</text>
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">hello 👍</text>
|
||||
<text x="882" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="9" lengthAdjust="spacingAndGlyphs">👍</text>
|
||||
<rect x="45" y="17" width="9" height="17" fill="#ffffff" />
|
||||
<text x="882" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">hello</text>
|
||||
<rect x="81" y="17" width="9" height="17" fill="#ffffff" />
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">hello </text>
|
||||
<rect x="90" y="17" width="9" height="17" fill="#ffffff" />
|
||||
<text x="90" y="19" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">👍</text>
|
||||
<text x="99" y="19" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs"> world</text>
|
||||
<text x="882" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">hel</text>
|
||||
<rect x="63" y="17" width="9" height="17" fill="#ffffff" />
|
||||
<text x="63" y="19" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">l</text>
|
||||
<text x="72" y="19" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">o world</text>
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">run </text>
|
||||
<text x="72" y="19" fill="#d7afff" textLength="45" lengthAdjust="spacingAndGlyphs">@path</text>
|
||||
<rect x="117" y="17" width="9" height="17" fill="#d7afff" />
|
||||
<text x="117" y="19" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">/</text>
|
||||
<text x="126" y="19" fill="#d7afff" textLength="63" lengthAdjust="spacingAndGlyphs">to/file</text>
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">hello</text>
|
||||
<rect x="81" y="17" width="9" height="17" fill="#ffffff" />
|
||||
<text x="90" y="19" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">world</text>
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<rect x="36" y="17" width="9" height="17" fill="#ffffff" />
|
||||
<text x="45" y="19" fill="#afafaf" textLength="315" lengthAdjust="spacingAndGlyphs"> Type your message or @path/to/file</text>
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="105" viewBox="0 0 920 105">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="105" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">hello</text>
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="53" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">world</text>
|
||||
<rect x="81" y="51" width="9" height="17" fill="#ffffff" />
|
||||
<text x="891" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<rect x="0" y="0" width="900" height="17" fill="#1f1f1f" />
|
||||
<text x="0" y="2" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀</text>
|
||||
<rect x="0" y="17" width="9" height="17" fill="#1f1f1f" />
|
||||
<rect x="9" y="17" width="18" height="17" fill="#1f1f1f" />
|
||||
<text x="9" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<rect x="27" y="17" width="324" height="17" fill="#1f1f1f" />
|
||||
<text x="27" y="19" fill="#afafaf" textLength="324" lengthAdjust="spacingAndGlyphs"> Type your message or @path/to/file</text>
|
||||
<rect x="351" y="17" width="549" height="17" fill="#1f1f1f" />
|
||||
<rect x="0" y="34" width="900" height="17" fill="#1f1f1f" />
|
||||
<text x="0" y="36" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,5 +1,95 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor correctly 'at the beginning of a line' in a multiline block 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > first line │
|
||||
│ second line │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor correctly 'at the end of a line' in a multiline block 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > first line │
|
||||
│ second line │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor correctly 'in the middle of a line' in a multiline block 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > first line │
|
||||
│ second line │
|
||||
│ third line │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor on a blank line in a multiline block 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > first line │
|
||||
│ │
|
||||
│ third line │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'after multi-byte unicode characters' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > 👍A │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the beginning of the line' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the end of a line with unicode cha…' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello 👍 │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the end of a short line with unico…' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > 👍 │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the end of the line' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'for multi-byte unicode characters' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello 👍 world │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'mid-word' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello world │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'on a highlighted token' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > run @path/to/file │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'on a space between words' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello world │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'on an empty line' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > Type your message or @path/to/file │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > History Navigation and Completion Suppression > should not render suggestions during history navigation 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> second message
|
||||
@@ -78,11 +168,18 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > multiline rendering > should correctly render multiline input including blank lines 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello │
|
||||
│ │
|
||||
│ world │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = `
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
@@ -180,7 +180,6 @@ export interface UIState {
|
||||
contextFileNames: string[];
|
||||
errorCount: number;
|
||||
availableTerminalHeight: number | undefined;
|
||||
stableControlsHeight: number;
|
||||
mainAreaWidth: number;
|
||||
staticAreaMaxItemHeight: number;
|
||||
staticExtraHeight: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
useCommandCompletion,
|
||||
CompletionMode,
|
||||
} from './useCommandCompletion.js';
|
||||
import type { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import type { CommandContext } from '../commands/types.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { useTextBuffer } from '../components/shared/text-buffer.js';
|
||||
import type { Suggestion } from '../components/SuggestionsDisplay.js';
|
||||
@@ -72,11 +72,7 @@ const setupMocks = ({
|
||||
shellSuggestions = [],
|
||||
isLoading = false,
|
||||
isPerfectMatch = false,
|
||||
slashCompletionRange = {
|
||||
completionStart: 0,
|
||||
completionEnd: 0,
|
||||
getCommandFromSuggestion: () => undefined,
|
||||
},
|
||||
slashCompletionRange = { completionStart: 0, completionEnd: 0 },
|
||||
shellCompletionRange = {
|
||||
completionStart: 0,
|
||||
completionEnd: 0,
|
||||
@@ -89,13 +85,7 @@ const setupMocks = ({
|
||||
shellSuggestions?: Suggestion[];
|
||||
isLoading?: boolean;
|
||||
isPerfectMatch?: boolean;
|
||||
slashCompletionRange?: {
|
||||
completionStart: number;
|
||||
completionEnd: number;
|
||||
getCommandFromSuggestion: (
|
||||
suggestion: Suggestion,
|
||||
) => SlashCommand | undefined;
|
||||
};
|
||||
slashCompletionRange?: { completionStart: number; completionEnd: number };
|
||||
shellCompletionRange?: {
|
||||
completionStart: number;
|
||||
completionEnd: number;
|
||||
@@ -481,15 +471,10 @@ describe('useCommandCompletion', () => {
|
||||
});
|
||||
|
||||
describe('handleAutocomplete', () => {
|
||||
it('should complete a partial command and NOT add a space if it has an action', async () => {
|
||||
it('should complete a partial command', async () => {
|
||||
setupMocks({
|
||||
slashSuggestions: [{ label: 'memory', value: 'memory' }],
|
||||
slashCompletionRange: {
|
||||
completionStart: 1,
|
||||
completionEnd: 4,
|
||||
getCommandFromSuggestion: () =>
|
||||
({ action: vi.fn() }) as unknown as SlashCommand,
|
||||
},
|
||||
slashCompletionRange: { completionStart: 1, completionEnd: 4 },
|
||||
});
|
||||
|
||||
const { result } = await renderCommandCompletionHook('/mem');
|
||||
@@ -502,40 +487,12 @@ describe('useCommandCompletion', () => {
|
||||
result.current.handleAutocomplete(0);
|
||||
});
|
||||
|
||||
expect(result.current.textBuffer.text).toBe('/memory');
|
||||
});
|
||||
|
||||
it('should complete a partial command and ADD a space if it has NO action (e.g. just a parent)', async () => {
|
||||
setupMocks({
|
||||
slashSuggestions: [{ label: 'chat', value: 'chat' }],
|
||||
slashCompletionRange: {
|
||||
completionStart: 1,
|
||||
completionEnd: 5,
|
||||
getCommandFromSuggestion: () => ({}) as unknown as SlashCommand, // No action
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = await renderCommandCompletionHook('/chat');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleAutocomplete(0);
|
||||
});
|
||||
|
||||
expect(result.current.textBuffer.text).toBe('/chat ');
|
||||
expect(result.current.textBuffer.text).toBe('/memory ');
|
||||
});
|
||||
|
||||
it('should complete a file path', async () => {
|
||||
setupMocks({
|
||||
atSuggestions: [{ label: 'src/file1.txt', value: 'src/file1.txt' }],
|
||||
slashCompletionRange: {
|
||||
completionStart: 0,
|
||||
completionEnd: 0,
|
||||
getCommandFromSuggestion: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = await renderCommandCompletionHook('@src/fi');
|
||||
@@ -560,11 +517,7 @@ describe('useCommandCompletion', () => {
|
||||
insertValue: 'resume list',
|
||||
},
|
||||
],
|
||||
slashCompletionRange: {
|
||||
completionStart: 1,
|
||||
completionEnd: 5,
|
||||
getCommandFromSuggestion: () => undefined,
|
||||
},
|
||||
slashCompletionRange: { completionStart: 1, completionEnd: 5 },
|
||||
});
|
||||
|
||||
const { result } = await renderCommandCompletionHook('/resu');
|
||||
@@ -586,11 +539,6 @@ describe('useCommandCompletion', () => {
|
||||
|
||||
setupMocks({
|
||||
atSuggestions: [{ label: 'src/file1.txt', value: 'src/file1.txt' }],
|
||||
slashCompletionRange: {
|
||||
completionStart: 0,
|
||||
completionEnd: 0,
|
||||
getCommandFromSuggestion: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = await renderCommandCompletionHook(text, cursorOffset);
|
||||
@@ -611,11 +559,6 @@ describe('useCommandCompletion', () => {
|
||||
it('should complete a directory path ending with / without a trailing space', async () => {
|
||||
setupMocks({
|
||||
atSuggestions: [{ label: 'src/components/', value: 'src/components/' }],
|
||||
slashCompletionRange: {
|
||||
completionStart: 0,
|
||||
completionEnd: 0,
|
||||
getCommandFromSuggestion: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = await renderCommandCompletionHook('@src/comp');
|
||||
@@ -636,11 +579,6 @@ describe('useCommandCompletion', () => {
|
||||
atSuggestions: [
|
||||
{ label: 'src\\components\\', value: 'src\\components\\' },
|
||||
],
|
||||
slashCompletionRange: {
|
||||
completionStart: 0,
|
||||
completionEnd: 0,
|
||||
getCommandFromSuggestion: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = await renderCommandCompletionHook('@src\\comp');
|
||||
@@ -656,33 +594,6 @@ describe('useCommandCompletion', () => {
|
||||
expect(result.current.textBuffer.text).toBe('@src\\components\\');
|
||||
});
|
||||
|
||||
it('should ADD a space for AT completion even if name matches a command with an action', async () => {
|
||||
// Setup a mock where getCommandFromSuggestion WOULD return a command with an action
|
||||
// if it were in SLASH mode.
|
||||
setupMocks({
|
||||
atSuggestions: [{ label: 'memory', value: 'memory' }],
|
||||
slashCompletionRange: {
|
||||
completionStart: 0,
|
||||
completionEnd: 0,
|
||||
getCommandFromSuggestion: () =>
|
||||
({ action: vi.fn() }) as unknown as SlashCommand,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = await renderCommandCompletionHook('@mem');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBe(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleAutocomplete(0);
|
||||
});
|
||||
|
||||
// Should have a space because it's AT mode, not SLASH mode
|
||||
expect(result.current.textBuffer.text).toBe('@memory ');
|
||||
});
|
||||
|
||||
it('should show ghost text for a single shell completion', async () => {
|
||||
const text = 'l';
|
||||
setupMocks({
|
||||
@@ -994,11 +905,6 @@ describe('useCommandCompletion', () => {
|
||||
it('should complete file path and add trailing space', async () => {
|
||||
setupMocks({
|
||||
atSuggestions: [{ label: 'src/file.txt', value: 'src/file.txt' }],
|
||||
slashCompletionRange: {
|
||||
completionStart: 0,
|
||||
completionEnd: 0,
|
||||
getCommandFromSuggestion: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = await renderCommandCompletionHook('/cmd @src/fi');
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback, useMemo, useEffect, useState } from 'react';
|
||||
import type { Suggestion } from '../components/SuggestionsDisplay.js';
|
||||
import type { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import type { TextBuffer } from '../components/shared/text-buffer.js';
|
||||
import { logicalPosToOffset } from '../components/shared/text-buffer.js';
|
||||
import { toCodePoints } from '../utils/textUtils.js';
|
||||
import { isSlashCommand } from '../utils/commandUtils.js';
|
||||
import { toCodePoints } from '../utils/textUtils.js';
|
||||
import { useAtCompletion } from './useAtCompletion.js';
|
||||
import { useSlashCompletion } from './useSlashCompletion.js';
|
||||
import { useShellCompletion } from './useShellCompletion.js';
|
||||
@@ -437,23 +436,10 @@ export function useCommandCompletion({
|
||||
|
||||
const lineCodePoints = toCodePoints(buffer.lines[cursorRow] || '');
|
||||
const charAfterCompletion = lineCodePoints[end];
|
||||
|
||||
let shouldAddSpace = true;
|
||||
if (completionMode === CompletionMode.SLASH) {
|
||||
const command =
|
||||
slashCompletionRange.getCommandFromSuggestion(suggestion);
|
||||
// Don't add a space if the command has an action (can be executed)
|
||||
// and doesn't have a completion function (doesn't REQUIRE more arguments)
|
||||
const isExecutableCommand = !!(command && command.action);
|
||||
const requiresArguments = !!(command && command.completion);
|
||||
shouldAddSpace = !isExecutableCommand || requiresArguments;
|
||||
}
|
||||
|
||||
if (
|
||||
charAfterCompletion !== ' ' &&
|
||||
!suggestionText.endsWith('/') &&
|
||||
!suggestionText.endsWith('\\') &&
|
||||
shouldAddSpace
|
||||
!suggestionText.endsWith('\\')
|
||||
) {
|
||||
suggestionText += ' ';
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { CoreToolCallStatus, ApprovalMode } from '@google/gemini-cli-core';
|
||||
import { type HistoryItemToolGroup, StreamingState } from '../types.js';
|
||||
import { INTERACTIVE_SHELL_WAITING_PHRASE } from './usePhraseCycler.js';
|
||||
import { isContextUsageHigh } from '../utils/contextUsage.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
/**
|
||||
* A hook that encapsulates complex status and action-required logic for the Composer.
|
||||
*/
|
||||
export const useComposerStatus = () => {
|
||||
const uiState = useUIState();
|
||||
const settings = useSettings();
|
||||
|
||||
const hasPendingToolConfirmation = useMemo(
|
||||
() =>
|
||||
(uiState.pendingHistoryItems ?? [])
|
||||
.filter(
|
||||
(item): item is HistoryItemToolGroup => item.type === 'tool_group',
|
||||
)
|
||||
.some((item) =>
|
||||
item.tools.some(
|
||||
(tool) => tool.status === CoreToolCallStatus.AwaitingApproval,
|
||||
),
|
||||
),
|
||||
[uiState.pendingHistoryItems],
|
||||
);
|
||||
|
||||
const hasPendingActionRequired =
|
||||
hasPendingToolConfirmation ||
|
||||
Boolean(uiState.commandConfirmationRequest) ||
|
||||
Boolean(uiState.authConsentRequest) ||
|
||||
(uiState.confirmUpdateExtensionRequests?.length ?? 0) > 0 ||
|
||||
Boolean(uiState.loopDetectionConfirmationRequest) ||
|
||||
Boolean(uiState.quota.proQuotaRequest) ||
|
||||
Boolean(uiState.quota.validationRequest) ||
|
||||
Boolean(uiState.customDialog);
|
||||
|
||||
const isInteractiveShellWaiting = Boolean(
|
||||
uiState.currentLoadingPhrase?.includes(INTERACTIVE_SHELL_WAITING_PHRASE),
|
||||
);
|
||||
|
||||
const showLoadingIndicator =
|
||||
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
|
||||
uiState.streamingState === StreamingState.Responding &&
|
||||
!hasPendingActionRequired;
|
||||
|
||||
const showApprovalModeIndicator = uiState.showApprovalModeIndicator;
|
||||
|
||||
const modeContentObj = useMemo(() => {
|
||||
const hideMinimalModeHintWhileBusy =
|
||||
!uiState.cleanUiDetailsVisible &&
|
||||
(showLoadingIndicator || uiState.activeHooks.length > 0);
|
||||
|
||||
if (hideMinimalModeHintWhileBusy) return null;
|
||||
|
||||
switch (showApprovalModeIndicator) {
|
||||
case ApprovalMode.YOLO:
|
||||
return { text: 'YOLO', color: theme.status.error };
|
||||
case ApprovalMode.PLAN:
|
||||
return { text: 'plan', color: theme.status.success };
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
return { text: 'auto edit', color: theme.status.warning };
|
||||
case ApprovalMode.DEFAULT:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [
|
||||
uiState.cleanUiDetailsVisible,
|
||||
showLoadingIndicator,
|
||||
uiState.activeHooks.length,
|
||||
showApprovalModeIndicator,
|
||||
]);
|
||||
|
||||
const showMinimalContext = isContextUsageHigh(
|
||||
uiState.sessionStats.lastPromptTokenCount,
|
||||
uiState.currentModel,
|
||||
settings.merged.model?.compressionThreshold,
|
||||
);
|
||||
|
||||
const loadingPhrases = settings.merged.ui.loadingPhrases;
|
||||
const showTips = loadingPhrases === 'tips' || loadingPhrases === 'all';
|
||||
const showWit = loadingPhrases === 'witty' || loadingPhrases === 'all';
|
||||
|
||||
/**
|
||||
* Use the setting if provided, otherwise default to true for the new UX.
|
||||
* This allows tests to override the collapse behavior.
|
||||
*/
|
||||
const shouldCollapseDuringApproval =
|
||||
settings.merged.ui.collapseDrawerDuringApproval !== false;
|
||||
|
||||
return {
|
||||
hasPendingActionRequired,
|
||||
shouldCollapseDuringApproval,
|
||||
isInteractiveShellWaiting,
|
||||
showLoadingIndicator,
|
||||
showTips,
|
||||
showWit,
|
||||
modeContentObj,
|
||||
showMinimalContext,
|
||||
};
|
||||
};
|
||||
@@ -7,76 +7,93 @@
|
||||
import { act, useCallback } from 'react';
|
||||
import { vi } from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useConsoleMessages } from './useConsoleMessages.js';
|
||||
import { CoreEvent, type ConsoleLogPayload } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock coreEvents
|
||||
let consoleLogHandler: ((payload: ConsoleLogPayload) => void) | undefined;
|
||||
import {
|
||||
useConsoleMessages,
|
||||
useErrorCount,
|
||||
initializeConsoleStore,
|
||||
} from './useConsoleMessages.js';
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const actual = (await importOriginal()) as any;
|
||||
const actual = await importOriginal();
|
||||
const handlers = new Map<string, (payload: unknown) => void>();
|
||||
|
||||
return {
|
||||
...actual,
|
||||
...(actual as Record<string, unknown>),
|
||||
coreEvents: {
|
||||
on: vi.fn((event, handler) => {
|
||||
if (event === CoreEvent.ConsoleLog) {
|
||||
consoleLogHandler = handler;
|
||||
}
|
||||
...((actual as Record<string, unknown>)['coreEvents'] as Record<
|
||||
string,
|
||||
unknown
|
||||
>),
|
||||
on: vi.fn((event: string, handler: (payload: unknown) => void) => {
|
||||
handlers.set(event, handler);
|
||||
}),
|
||||
off: vi.fn((event) => {
|
||||
if (event === CoreEvent.ConsoleLog) {
|
||||
consoleLogHandler = undefined;
|
||||
}
|
||||
off: vi.fn((event: string) => {
|
||||
handlers.delete(event);
|
||||
}),
|
||||
emitConsoleLog: vi.fn(),
|
||||
// Helper for testing to trigger the handlers
|
||||
_trigger: (event: string, payload: unknown) => {
|
||||
handlers.get(event)?.(payload);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('useConsoleMessages', () => {
|
||||
let unmounts: Array<() => void> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
consoleLogHandler = undefined;
|
||||
initializeConsoleStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const unmount of unmounts) {
|
||||
try {
|
||||
unmount();
|
||||
} catch (_e) {
|
||||
// Ignore unmount errors
|
||||
}
|
||||
}
|
||||
unmounts = [];
|
||||
vi.runOnlyPendingTimers();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const useTestableConsoleMessages = () => {
|
||||
const { ...rest } = useConsoleMessages();
|
||||
const consoleMessages = useConsoleMessages();
|
||||
const log = useCallback((content: string) => {
|
||||
if (consoleLogHandler) {
|
||||
consoleLogHandler({ type: 'log', content });
|
||||
}
|
||||
// @ts-expect-error - internal testing helper
|
||||
coreEvents._trigger('console-log', { type: 'log', content });
|
||||
}, []);
|
||||
const error = useCallback((content: string) => {
|
||||
if (consoleLogHandler) {
|
||||
consoleLogHandler({ type: 'error', content });
|
||||
}
|
||||
// @ts-expect-error - internal testing helper
|
||||
coreEvents._trigger('console-log', { type: 'error', content });
|
||||
}, []);
|
||||
const clearConsoleMessages = useCallback(() => {
|
||||
initializeConsoleStore();
|
||||
}, []);
|
||||
return {
|
||||
...rest,
|
||||
consoleMessages,
|
||||
log,
|
||||
error,
|
||||
clearConsoleMessages: rest.clearConsoleMessages,
|
||||
clearConsoleMessages,
|
||||
};
|
||||
};
|
||||
|
||||
const renderConsoleMessagesHook = async () => {
|
||||
let hookResult: ReturnType<typeof useTestableConsoleMessages>;
|
||||
let hookResult: ReturnType<typeof useTestableConsoleMessages> | undefined;
|
||||
function TestComponent() {
|
||||
hookResult = useTestableConsoleMessages();
|
||||
return null;
|
||||
}
|
||||
const { unmount } = await render(<TestComponent />);
|
||||
unmounts.push(unmount);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
return hookResult!;
|
||||
},
|
||||
},
|
||||
unmount,
|
||||
@@ -93,10 +110,7 @@ describe('useConsoleMessages', () => {
|
||||
|
||||
act(() => {
|
||||
result.current.log('Test message');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(60);
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(result.current.consoleMessages).toEqual([
|
||||
@@ -111,10 +125,7 @@ describe('useConsoleMessages', () => {
|
||||
result.current.log('Test message');
|
||||
result.current.log('Test message');
|
||||
result.current.log('Test message');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(60);
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(result.current.consoleMessages).toEqual([
|
||||
@@ -128,10 +139,7 @@ describe('useConsoleMessages', () => {
|
||||
act(() => {
|
||||
result.current.log('First message');
|
||||
result.current.error('Second message');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(60);
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(result.current.consoleMessages).toEqual([
|
||||
@@ -139,53 +147,85 @@ describe('useConsoleMessages', () => {
|
||||
{ type: 'error', content: 'Second message', count: 1 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear all messages when clearConsoleMessages is called', async () => {
|
||||
const { result } = await renderConsoleMessagesHook();
|
||||
describe('useErrorCount', () => {
|
||||
let unmounts: Array<() => void> = [];
|
||||
|
||||
act(() => {
|
||||
result.current.log('A message');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(60);
|
||||
});
|
||||
|
||||
expect(result.current.consoleMessages).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
result.current.clearConsoleMessages();
|
||||
});
|
||||
|
||||
expect(result.current.consoleMessages).toHaveLength(0);
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
initializeConsoleStore();
|
||||
});
|
||||
|
||||
it('should clear the pending timeout when clearConsoleMessages is called', async () => {
|
||||
const { result } = await renderConsoleMessagesHook();
|
||||
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
|
||||
|
||||
act(() => {
|
||||
result.current.log('A message');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.clearConsoleMessages();
|
||||
});
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled();
|
||||
// clearTimeoutSpy.mockRestore() is handled by afterEach restoreAllMocks
|
||||
afterEach(() => {
|
||||
for (const unmount of unmounts) {
|
||||
try {
|
||||
unmount();
|
||||
} catch (_e) {
|
||||
// Ignore unmount errors
|
||||
}
|
||||
}
|
||||
unmounts = [];
|
||||
vi.runOnlyPendingTimers();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should clean up the timeout on unmount', async () => {
|
||||
const { result, unmount } = await renderConsoleMessagesHook();
|
||||
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
|
||||
const renderErrorCountHook = async () => {
|
||||
let hookResult: ReturnType<typeof useErrorCount>;
|
||||
function TestComponent() {
|
||||
hookResult = useErrorCount();
|
||||
return null;
|
||||
}
|
||||
const { unmount } = await render(<TestComponent />);
|
||||
unmounts.push(unmount);
|
||||
return {
|
||||
result: {
|
||||
get current() {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
unmount,
|
||||
};
|
||||
};
|
||||
|
||||
it('should initialize with an error count of 0', async () => {
|
||||
const { result } = await renderErrorCountHook();
|
||||
expect(result.current.errorCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should increment error count when an error is logged', async () => {
|
||||
const { result } = await renderErrorCountHook();
|
||||
act(() => {
|
||||
// @ts-expect-error - internal testing helper
|
||||
coreEvents._trigger('console-log', { type: 'error', content: 'error' });
|
||||
vi.runAllTimers();
|
||||
});
|
||||
expect(result.current.errorCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should not increment error count for non-error logs', async () => {
|
||||
const { result } = await renderErrorCountHook();
|
||||
act(() => {
|
||||
// @ts-expect-error - internal testing helper
|
||||
coreEvents._trigger('console-log', { type: 'log', content: 'log' });
|
||||
vi.runAllTimers();
|
||||
});
|
||||
expect(result.current.errorCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should clear the error count', async () => {
|
||||
const { result } = await renderErrorCountHook();
|
||||
act(() => {
|
||||
// @ts-expect-error - internal testing helper
|
||||
coreEvents._trigger('console-log', { type: 'error', content: 'error' });
|
||||
vi.runAllTimers();
|
||||
});
|
||||
expect(result.current.errorCount).toBe(1);
|
||||
|
||||
act(() => {
|
||||
result.current.log('A message');
|
||||
result.current.clearErrorCount();
|
||||
});
|
||||
|
||||
unmount();
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled();
|
||||
expect(result.current.errorCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useReducer,
|
||||
useRef,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import { useCallback, useSyncExternalStore } from 'react';
|
||||
import type { ConsoleMessageItem } from '../types.js';
|
||||
import {
|
||||
coreEvents,
|
||||
@@ -18,207 +12,170 @@ import {
|
||||
type ConsoleLogPayload,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export interface UseConsoleMessagesReturn {
|
||||
consoleMessages: ConsoleMessageItem[];
|
||||
clearConsoleMessages: () => void;
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'ADD_MESSAGES'; payload: ConsoleMessageItem[] }
|
||||
| { type: 'CLEAR' };
|
||||
|
||||
function consoleMessagesReducer(
|
||||
state: ConsoleMessageItem[],
|
||||
action: Action,
|
||||
): ConsoleMessageItem[] {
|
||||
const MAX_CONSOLE_MESSAGES = 1000;
|
||||
switch (action.type) {
|
||||
case 'ADD_MESSAGES': {
|
||||
const newMessages = [...state];
|
||||
for (const queuedMessage of action.payload) {
|
||||
const lastMessage = newMessages[newMessages.length - 1];
|
||||
if (
|
||||
lastMessage &&
|
||||
lastMessage.type === queuedMessage.type &&
|
||||
lastMessage.content === queuedMessage.content
|
||||
) {
|
||||
// Create a new object for the last message to ensure React detects
|
||||
// the change, preventing mutation of the existing state object.
|
||||
newMessages[newMessages.length - 1] = {
|
||||
...lastMessage,
|
||||
count: lastMessage.count + 1,
|
||||
};
|
||||
} else {
|
||||
newMessages.push({ ...queuedMessage, count: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
// Limit the number of messages to prevent memory issues
|
||||
if (newMessages.length > MAX_CONSOLE_MESSAGES) {
|
||||
return newMessages.slice(newMessages.length - MAX_CONSOLE_MESSAGES);
|
||||
}
|
||||
|
||||
return newMessages;
|
||||
}
|
||||
case 'CLEAR':
|
||||
return [];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function useConsoleMessages(): UseConsoleMessagesReturn {
|
||||
const [consoleMessages, dispatch] = useReducer(consoleMessagesReducer, []);
|
||||
const messageQueueRef = useRef<ConsoleMessageItem[]>([]);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isProcessingRef = useRef(false);
|
||||
|
||||
const processQueue = useCallback(() => {
|
||||
if (messageQueueRef.current.length > 0) {
|
||||
isProcessingRef.current = true;
|
||||
const messagesToProcess = messageQueueRef.current;
|
||||
messageQueueRef.current = [];
|
||||
startTransition(() => {
|
||||
dispatch({ type: 'ADD_MESSAGES', payload: messagesToProcess });
|
||||
});
|
||||
}
|
||||
timeoutRef.current = null;
|
||||
}, []);
|
||||
|
||||
const handleNewMessage = useCallback(
|
||||
(message: ConsoleMessageItem) => {
|
||||
messageQueueRef.current.push(message);
|
||||
if (!isProcessingRef.current && !timeoutRef.current) {
|
||||
// Batch updates using a timeout. 50ms is a reasonable delay to batch
|
||||
// rapid-fire messages without noticeable lag while avoiding React update
|
||||
// queue flooding.
|
||||
timeoutRef.current = setTimeout(processQueue, 50);
|
||||
}
|
||||
},
|
||||
[processQueue],
|
||||
);
|
||||
|
||||
// Once the updated consoleMessages have been committed to the screen,
|
||||
// we can safely process the next batch of queued messages if any exist.
|
||||
// This completely eliminates overlapping concurrent updates to this state.
|
||||
useEffect(() => {
|
||||
isProcessingRef.current = false;
|
||||
if (messageQueueRef.current.length > 0 && !timeoutRef.current) {
|
||||
timeoutRef.current = setTimeout(processQueue, 50);
|
||||
}
|
||||
}, [consoleMessages, processQueue]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleConsoleLog = (payload: ConsoleLogPayload) => {
|
||||
let content = payload.content;
|
||||
const MAX_CONSOLE_MSG_LENGTH = 10000;
|
||||
if (content.length > MAX_CONSOLE_MSG_LENGTH) {
|
||||
content =
|
||||
content.slice(0, MAX_CONSOLE_MSG_LENGTH) +
|
||||
`... [Truncated ${content.length - MAX_CONSOLE_MSG_LENGTH} characters]`;
|
||||
}
|
||||
|
||||
handleNewMessage({
|
||||
type: payload.type,
|
||||
content,
|
||||
count: 1,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOutput = (payload: {
|
||||
isStderr: boolean;
|
||||
chunk: Uint8Array | string;
|
||||
}) => {
|
||||
let content =
|
||||
typeof payload.chunk === 'string'
|
||||
? payload.chunk
|
||||
: new TextDecoder().decode(payload.chunk);
|
||||
|
||||
const MAX_OUTPUT_CHUNK_LENGTH = 10000;
|
||||
if (content.length > MAX_OUTPUT_CHUNK_LENGTH) {
|
||||
content =
|
||||
content.slice(0, MAX_OUTPUT_CHUNK_LENGTH) +
|
||||
`... [Truncated ${content.length - MAX_OUTPUT_CHUNK_LENGTH} characters]`;
|
||||
}
|
||||
|
||||
// It would be nice if we could show stderr as 'warn' but unfortunately
|
||||
// we log non warning info to stderr before the app starts so that would
|
||||
// be misleading.
|
||||
handleNewMessage({ type: 'log', content, count: 1 });
|
||||
};
|
||||
|
||||
coreEvents.on(CoreEvent.ConsoleLog, handleConsoleLog);
|
||||
coreEvents.on(CoreEvent.Output, handleOutput);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.ConsoleLog, handleConsoleLog);
|
||||
coreEvents.off(CoreEvent.Output, handleOutput);
|
||||
};
|
||||
}, [handleNewMessage]);
|
||||
|
||||
const clearConsoleMessages = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
messageQueueRef.current = [];
|
||||
isProcessingRef.current = true;
|
||||
startTransition(() => {
|
||||
dispatch({ type: 'CLEAR' });
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { consoleMessages, clearConsoleMessages };
|
||||
}
|
||||
|
||||
export interface UseErrorCountReturn {
|
||||
errorCount: number;
|
||||
clearErrorCount: () => void;
|
||||
}
|
||||
|
||||
// --- Global Console Store ---
|
||||
|
||||
const MAX_CONSOLE_MESSAGES = 1000;
|
||||
let globalConsoleMessages: ConsoleMessageItem[] = [];
|
||||
let globalErrorCount = 0;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
let messageQueue: ConsoleMessageItem[] = [];
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
/**
|
||||
* Initializes the global console store and subscribes to coreEvents.
|
||||
* Acts as a safe reset function, making it idempotent and useful for test isolation.
|
||||
* Must be called during application startup.
|
||||
*/
|
||||
export function initializeConsoleStore() {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
messageQueue = [];
|
||||
globalConsoleMessages = [];
|
||||
globalErrorCount = 0;
|
||||
notifyListeners();
|
||||
|
||||
// Safely detach first to ensure idempotency and prevent listener leaks
|
||||
coreEvents.off(CoreEvent.ConsoleLog, handleConsoleLog);
|
||||
coreEvents.off(CoreEvent.Output, handleOutput);
|
||||
|
||||
coreEvents.on(CoreEvent.ConsoleLog, handleConsoleLog);
|
||||
coreEvents.on(CoreEvent.Output, handleOutput);
|
||||
}
|
||||
|
||||
function notifyListeners() {
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
if (messageQueue.length === 0) return;
|
||||
|
||||
// Create a new array to trigger React updates
|
||||
const newMessages = [...globalConsoleMessages];
|
||||
|
||||
for (const queuedMessage of messageQueue) {
|
||||
if (queuedMessage.type === 'error') {
|
||||
globalErrorCount++;
|
||||
}
|
||||
|
||||
// Coalesce consecutive identical messages
|
||||
const prev = newMessages[newMessages.length - 1];
|
||||
if (
|
||||
prev &&
|
||||
prev.type === queuedMessage.type &&
|
||||
prev.content === queuedMessage.content
|
||||
) {
|
||||
newMessages[newMessages.length - 1] = {
|
||||
...prev,
|
||||
count: prev.count + 1,
|
||||
};
|
||||
} else {
|
||||
newMessages.push({ ...queuedMessage, count: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
globalConsoleMessages =
|
||||
newMessages.length > MAX_CONSOLE_MESSAGES
|
||||
? newMessages.slice(-MAX_CONSOLE_MESSAGES)
|
||||
: newMessages;
|
||||
|
||||
messageQueue = [];
|
||||
timeoutId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
function handleNewMessage(message: ConsoleMessageItem) {
|
||||
messageQueue.push(message);
|
||||
if (!timeoutId) {
|
||||
// Batch updates using a timeout. 50ms is a reasonable delay to batch
|
||||
// rapid-fire messages without noticeable lag while avoiding React update
|
||||
// queue flooding.
|
||||
timeoutId = setTimeout(processQueue, 50);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Subscription API for useSyncExternalStore ---
|
||||
|
||||
function subscribe(listener: () => void) {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
function getConsoleMessagesSnapshot() {
|
||||
return globalConsoleMessages;
|
||||
}
|
||||
|
||||
function getErrorCountSnapshot() {
|
||||
return globalErrorCount;
|
||||
}
|
||||
|
||||
// --- Core Event Listeners (Always active at module level) ---
|
||||
|
||||
const handleConsoleLog = (payload: ConsoleLogPayload) => {
|
||||
let content = payload.content;
|
||||
const MAX_CONSOLE_MSG_LENGTH = 10000;
|
||||
if (content.length > MAX_CONSOLE_MSG_LENGTH) {
|
||||
content =
|
||||
content.slice(0, MAX_CONSOLE_MSG_LENGTH) +
|
||||
`... [Truncated ${content.length - MAX_CONSOLE_MSG_LENGTH} characters]`;
|
||||
}
|
||||
|
||||
handleNewMessage({
|
||||
type: payload.type,
|
||||
content,
|
||||
count: 1,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOutput = (payload: {
|
||||
isStderr: boolean;
|
||||
chunk: Uint8Array | string;
|
||||
}) => {
|
||||
let content =
|
||||
typeof payload.chunk === 'string'
|
||||
? payload.chunk
|
||||
: new TextDecoder().decode(payload.chunk);
|
||||
|
||||
const MAX_OUTPUT_CHUNK_LENGTH = 10000;
|
||||
if (content.length > MAX_OUTPUT_CHUNK_LENGTH) {
|
||||
content =
|
||||
content.slice(0, MAX_OUTPUT_CHUNK_LENGTH) +
|
||||
`... [Truncated ${content.length - MAX_OUTPUT_CHUNK_LENGTH} characters]`;
|
||||
}
|
||||
|
||||
handleNewMessage({ type: 'log', content, count: 1 });
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to access the global console message history.
|
||||
* Decoupled from any component lifecycle to ensure history is preserved even
|
||||
* when the UI is unmounted.
|
||||
*/
|
||||
export function useConsoleMessages(): ConsoleMessageItem[] {
|
||||
return useSyncExternalStore(subscribe, getConsoleMessagesSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access the global error count.
|
||||
* Uses the same external store as useConsoleMessages for consistency.
|
||||
*/
|
||||
export function useErrorCount(): UseErrorCountReturn {
|
||||
const [errorCount, dispatch] = useReducer(
|
||||
(state: number, action: 'INCREMENT' | 'CLEAR') => {
|
||||
switch (action) {
|
||||
case 'INCREMENT':
|
||||
return state + 1;
|
||||
case 'CLEAR':
|
||||
return 0;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleConsoleLog = (payload: ConsoleLogPayload) => {
|
||||
if (payload.type === 'error') {
|
||||
startTransition(() => {
|
||||
dispatch('INCREMENT');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
coreEvents.on(CoreEvent.ConsoleLog, handleConsoleLog);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.ConsoleLog, handleConsoleLog);
|
||||
};
|
||||
}, []);
|
||||
const errorCount = useSyncExternalStore(subscribe, getErrorCountSnapshot);
|
||||
|
||||
const clearErrorCount = useCallback(() => {
|
||||
startTransition(() => {
|
||||
dispatch('CLEAR');
|
||||
});
|
||||
globalErrorCount = 0;
|
||||
notifyListeners();
|
||||
}, []);
|
||||
|
||||
return { errorCount, clearErrorCount };
|
||||
|
||||
@@ -66,11 +66,11 @@ export const usePhraseCycler = (
|
||||
|
||||
if (shouldShowFocusHint || isWaiting) {
|
||||
// These are handled by the return value directly for immediate feedback
|
||||
return clearTimers;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isActive || (!showTips && !showWit)) {
|
||||
return clearTimers;
|
||||
return;
|
||||
}
|
||||
|
||||
const wittyPhrasesList =
|
||||
@@ -101,7 +101,6 @@ export const usePhraseCycler = (
|
||||
: INFORMATIVE_TIPS;
|
||||
|
||||
if (filteredTips.length > 0) {
|
||||
// codeql[js/insecure-randomness] false positive: used for non-sensitive UI flavor text (tips)
|
||||
const selected =
|
||||
filteredTips[Math.floor(Math.random() * filteredTips.length)];
|
||||
setCurrentTipState(selected);
|
||||
@@ -133,7 +132,6 @@ export const usePhraseCycler = (
|
||||
: wittyPhrasesList;
|
||||
|
||||
if (filteredWitty.length > 0) {
|
||||
// codeql[js/insecure-randomness] false positive: used for non-sensitive UI flavor text (witty phrases)
|
||||
const selected =
|
||||
filteredWitty[Math.floor(Math.random() * filteredWitty.length)];
|
||||
setCurrentWittyPhraseState(selected);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@@ -464,7 +464,7 @@ describe('useSlashCompletion', () => {
|
||||
() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/chat ',
|
||||
'/chat',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
@@ -484,7 +484,7 @@ describe('useSlashCompletion', () => {
|
||||
() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/resume ',
|
||||
'/resume',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
@@ -513,6 +513,53 @@ describe('useSlashCompletion', () => {
|
||||
unmountResume();
|
||||
});
|
||||
|
||||
it('should show the grouped /resume menu for unique /resum prefix input', async () => {
|
||||
const slashCommands = [
|
||||
createTestCommand({
|
||||
name: 'resume',
|
||||
description: 'Resume command',
|
||||
action: vi.fn(),
|
||||
subCommands: [
|
||||
createTestCommand({
|
||||
name: 'list',
|
||||
description: 'List checkpoints',
|
||||
suggestionGroup: 'checkpoints',
|
||||
}),
|
||||
createTestCommand({
|
||||
name: 'save',
|
||||
description: 'Save checkpoint',
|
||||
suggestionGroup: 'checkpoints',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const { result, unmount } = await renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/resum',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
await resolveMatch();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions[0]).toMatchObject({
|
||||
label: 'list',
|
||||
sectionTitle: 'auto',
|
||||
submitValue: '/resume',
|
||||
});
|
||||
expect(result.current.isPerfectMatch).toBe(false);
|
||||
expect(result.current.suggestions.slice(1).map((s) => s.label)).toEqual(
|
||||
expect.arrayContaining(['list', 'save']),
|
||||
);
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should sort exact altName matches to the top', async () => {
|
||||
const slashCommands = [
|
||||
createTestCommand({
|
||||
@@ -547,7 +594,7 @@ describe('useSlashCompletion', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should suggest the command itself instead of subcommands when a parent command is fully typed without a trailing space', async () => {
|
||||
it('should suggest subcommands when a parent command is fully typed without a trailing space', async () => {
|
||||
const slashCommands = [
|
||||
createTestCommand({
|
||||
name: 'chat',
|
||||
@@ -571,47 +618,18 @@ describe('useSlashCompletion', () => {
|
||||
await resolveMatch();
|
||||
|
||||
await waitFor(() => {
|
||||
// Should show 'chat' as the suggestion, NOT its subcommands
|
||||
expect(result.current.suggestions).toHaveLength(1);
|
||||
expect(result.current.suggestions[0].label).toBe('chat');
|
||||
// completionStart should be at 1 (to replace 'chat')
|
||||
expect(result.current.completionStart).toBe(1);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT suggest subcommands when a parent command is fully typed without a trailing space (fix for over-eager completion)', async () => {
|
||||
const slashCommands = [
|
||||
createTestCommand({
|
||||
name: 'stats',
|
||||
description: 'Check session stats',
|
||||
action: vi.fn(), // Has action
|
||||
subCommands: [
|
||||
createTestCommand({
|
||||
name: 'session',
|
||||
description: 'Show session-specific usage statistics',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const { result, unmount } = await renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/stats',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
await resolveMatch();
|
||||
|
||||
await waitFor(() => {
|
||||
// Should show 'stats' as the suggestion, NOT 'session'
|
||||
expect(result.current.suggestions).toHaveLength(1);
|
||||
expect(result.current.suggestions[0].label).toBe('stats');
|
||||
// isPerfectMatch should be true because it has an action
|
||||
expect(result.current.isPerfectMatch).toBe(true);
|
||||
// Should show the auto-session entry plus subcommands of 'chat'
|
||||
expect(result.current.suggestions).toHaveLength(3);
|
||||
expect(result.current.suggestions[0]).toMatchObject({
|
||||
label: 'list',
|
||||
sectionTitle: 'auto',
|
||||
submitValue: '/chat',
|
||||
});
|
||||
expect(result.current.suggestions.map((s) => s.label)).toEqual(
|
||||
expect.arrayContaining(['list', 'save']),
|
||||
);
|
||||
// completionStart should be at the end of '/chat' to append subcommands
|
||||
expect(result.current.completionStart).toBe(5);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@@ -54,6 +54,8 @@ interface CommandParserResult {
|
||||
partial: string;
|
||||
currentLevel: readonly SlashCommand[] | undefined;
|
||||
leafCommand: SlashCommand | null;
|
||||
exactMatchAsParent: SlashCommand | undefined;
|
||||
usedPrefixParentDescent: boolean;
|
||||
isArgumentCompletion: boolean;
|
||||
}
|
||||
|
||||
@@ -69,6 +71,8 @@ function useCommandParser(
|
||||
partial: '',
|
||||
currentLevel: slashCommands,
|
||||
leafCommand: null,
|
||||
exactMatchAsParent: undefined,
|
||||
usedPrefixParentDescent: false,
|
||||
isArgumentCompletion: false,
|
||||
};
|
||||
}
|
||||
@@ -86,6 +90,7 @@ function useCommandParser(
|
||||
|
||||
let currentLevel: readonly SlashCommand[] | undefined = slashCommands;
|
||||
let leafCommand: SlashCommand | null = null;
|
||||
let usedPrefixParentDescent = false;
|
||||
|
||||
for (const part of commandPathParts) {
|
||||
if (!currentLevel) {
|
||||
@@ -110,6 +115,60 @@ function useCommandParser(
|
||||
}
|
||||
}
|
||||
|
||||
let exactMatchAsParent: SlashCommand | undefined;
|
||||
if (!hasTrailingSpace && currentLevel) {
|
||||
exactMatchAsParent = currentLevel.find(
|
||||
(cmd) => matchesCommand(cmd, partial) && cmd.subCommands,
|
||||
);
|
||||
|
||||
if (exactMatchAsParent) {
|
||||
// Only descend if there are NO other matches for the partial at this level.
|
||||
// This ensures that typing "/memory" still shows "/memory-leak" if it exists.
|
||||
const otherMatches = currentLevel.filter(
|
||||
(cmd) =>
|
||||
cmd !== exactMatchAsParent &&
|
||||
(cmd.name.toLowerCase().startsWith(partial.toLowerCase()) ||
|
||||
cmd.altNames?.some((alt) =>
|
||||
alt.toLowerCase().startsWith(partial.toLowerCase()),
|
||||
)),
|
||||
);
|
||||
|
||||
if (otherMatches.length === 0) {
|
||||
leafCommand = exactMatchAsParent;
|
||||
currentLevel = exactMatchAsParent.subCommands as
|
||||
| readonly SlashCommand[]
|
||||
| undefined;
|
||||
partial = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Phase-one alias UX: allow unique prefix descent for /chat and /resume
|
||||
// so `/cha` and `/resum` expose the same grouped menu immediately.
|
||||
if (!exactMatchAsParent && partial && currentLevel) {
|
||||
const prefixParentMatches = currentLevel.filter(
|
||||
(cmd) =>
|
||||
!!cmd.subCommands &&
|
||||
(cmd.name.toLowerCase().startsWith(partial.toLowerCase()) ||
|
||||
cmd.altNames?.some((alt) =>
|
||||
alt.toLowerCase().startsWith(partial.toLowerCase()),
|
||||
)),
|
||||
);
|
||||
|
||||
if (prefixParentMatches.length === 1) {
|
||||
const candidate = prefixParentMatches[0];
|
||||
if (candidate.name === 'chat' || candidate.name === 'resume') {
|
||||
exactMatchAsParent = candidate;
|
||||
leafCommand = candidate;
|
||||
usedPrefixParentDescent = true;
|
||||
currentLevel = candidate.subCommands as
|
||||
| readonly SlashCommand[]
|
||||
| undefined;
|
||||
partial = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const depth = commandPathParts.length;
|
||||
const isArgumentCompletion = !!(
|
||||
leafCommand?.completion &&
|
||||
@@ -123,6 +182,8 @@ function useCommandParser(
|
||||
partial,
|
||||
currentLevel,
|
||||
leafCommand,
|
||||
exactMatchAsParent,
|
||||
usedPrefixParentDescent,
|
||||
isArgumentCompletion,
|
||||
};
|
||||
}, [query, slashCommands]);
|
||||
@@ -282,9 +343,19 @@ function useCommandSuggestions(
|
||||
});
|
||||
|
||||
const finalSuggestions = sortedSuggestions.map((cmd) => {
|
||||
const canonicalParentName =
|
||||
parserResult.usedPrefixParentDescent &&
|
||||
leafCommand &&
|
||||
(leafCommand.name === 'chat' || leafCommand.name === 'resume')
|
||||
? leafCommand.name
|
||||
: undefined;
|
||||
|
||||
const suggestion: Suggestion = {
|
||||
label: cmd.name,
|
||||
value: cmd.name,
|
||||
insertValue: canonicalParentName
|
||||
? `${canonicalParentName} ${cmd.name}`
|
||||
: undefined,
|
||||
description: cmd.description,
|
||||
commandKind: cmd.kind,
|
||||
};
|
||||
@@ -313,7 +384,7 @@ function useCommandSuggestions(
|
||||
description: 'Browse auto-saved chats',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
sectionTitle: 'auto',
|
||||
submitValue: `/${canonicalParentName}`,
|
||||
submitValue: `/${leafCommand.name}`,
|
||||
};
|
||||
setSuggestions([autoSectionSuggestion, ...finalSuggestions]);
|
||||
return;
|
||||
@@ -356,10 +427,12 @@ function useCompletionPositions(
|
||||
return { start: -1, end: -1 };
|
||||
}
|
||||
|
||||
const { hasTrailingSpace, partial } = parserResult;
|
||||
const { hasTrailingSpace, partial, exactMatchAsParent } = parserResult;
|
||||
|
||||
// Set completion start/end positions
|
||||
if (hasTrailingSpace) {
|
||||
if (parserResult.usedPrefixParentDescent) {
|
||||
return { start: 1, end: query.length };
|
||||
} else if (hasTrailingSpace || exactMatchAsParent) {
|
||||
return { start: query.length, end: query.length };
|
||||
} else if (partial) {
|
||||
if (parserResult.isArgumentCompletion) {
|
||||
@@ -388,7 +461,12 @@ function usePerfectMatch(
|
||||
return { isPerfectMatch: false };
|
||||
}
|
||||
|
||||
if (leafCommand && partial === '' && leafCommand.action) {
|
||||
if (
|
||||
leafCommand &&
|
||||
partial === '' &&
|
||||
leafCommand.action &&
|
||||
!parserResult.usedPrefixParentDescent
|
||||
) {
|
||||
return { isPerfectMatch: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ export const DefaultAppLayout: React.FC = () => {
|
||||
flexDirection="column"
|
||||
width={uiState.terminalWidth}
|
||||
height={isAlternateBuffer ? terminalHeight : undefined}
|
||||
paddingBottom={isAlternateBuffer ? 1 : undefined}
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
overflow="hidden"
|
||||
@@ -63,9 +62,6 @@ export const DefaultAppLayout: React.FC = () => {
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
width={uiState.terminalWidth}
|
||||
height={
|
||||
uiState.copyModeEnabled ? uiState.stableControlsHeight : undefined
|
||||
}
|
||||
>
|
||||
<Notifications />
|
||||
<CopyModeWarning />
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { ConsolePatcher } from './ConsolePatcher.js';
|
||||
|
||||
describe('ConsolePatcher', () => {
|
||||
let patcher: ConsolePatcher;
|
||||
const onNewMessage = vi.fn();
|
||||
|
||||
afterEach(() => {
|
||||
if (patcher) {
|
||||
patcher.cleanup();
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should patch and restore console methods', () => {
|
||||
const beforeLog = console.log;
|
||||
const beforeWarn = console.warn;
|
||||
const beforeError = console.error;
|
||||
const beforeDebug = console.debug;
|
||||
const beforeInfo = console.info;
|
||||
|
||||
patcher = new ConsolePatcher({ onNewMessage, debugMode: false });
|
||||
patcher.patch();
|
||||
|
||||
expect(console.log).not.toBe(beforeLog);
|
||||
expect(console.warn).not.toBe(beforeWarn);
|
||||
expect(console.error).not.toBe(beforeError);
|
||||
expect(console.debug).not.toBe(beforeDebug);
|
||||
expect(console.info).not.toBe(beforeInfo);
|
||||
|
||||
patcher.cleanup();
|
||||
|
||||
expect(console.log).toBe(beforeLog);
|
||||
expect(console.warn).toBe(beforeWarn);
|
||||
expect(console.error).toBe(beforeError);
|
||||
expect(console.debug).toBe(beforeDebug);
|
||||
expect(console.info).toBe(beforeInfo);
|
||||
});
|
||||
|
||||
describe('Interactive mode', () => {
|
||||
it('should ignore log and info when it is not interactive and debugMode is false', () => {
|
||||
patcher = new ConsolePatcher({
|
||||
onNewMessage,
|
||||
debugMode: false,
|
||||
interactive: false,
|
||||
});
|
||||
patcher.patch();
|
||||
|
||||
console.log('test log');
|
||||
console.info('test info');
|
||||
expect(onNewMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not ignore log and info when it is not interactive and debugMode is true', () => {
|
||||
patcher = new ConsolePatcher({
|
||||
onNewMessage,
|
||||
debugMode: true,
|
||||
interactive: false,
|
||||
});
|
||||
patcher.patch();
|
||||
|
||||
console.log('test log');
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
type: 'log',
|
||||
content: 'test log',
|
||||
count: 1,
|
||||
});
|
||||
|
||||
console.info('test info');
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
type: 'info',
|
||||
content: 'test info',
|
||||
count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not ignore log and info when it is interactive', () => {
|
||||
patcher = new ConsolePatcher({
|
||||
onNewMessage,
|
||||
debugMode: false,
|
||||
interactive: true,
|
||||
});
|
||||
patcher.patch();
|
||||
|
||||
console.log('test log');
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
type: 'log',
|
||||
content: 'test log',
|
||||
count: 1,
|
||||
});
|
||||
|
||||
console.info('test info');
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
type: 'info',
|
||||
content: 'test info',
|
||||
count: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when stderr is false', () => {
|
||||
it('should call onNewMessage for log, warn, error, and info', () => {
|
||||
patcher = new ConsolePatcher({
|
||||
onNewMessage,
|
||||
debugMode: false,
|
||||
stderr: false,
|
||||
});
|
||||
patcher.patch();
|
||||
|
||||
console.log('test log');
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
type: 'log',
|
||||
content: 'test log',
|
||||
count: 1,
|
||||
});
|
||||
|
||||
console.warn('test warn');
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
type: 'warn',
|
||||
content: 'test warn',
|
||||
count: 1,
|
||||
});
|
||||
|
||||
console.error('test error');
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
content: 'test error',
|
||||
count: 1,
|
||||
});
|
||||
|
||||
console.info('test info');
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
type: 'info',
|
||||
content: 'test info',
|
||||
count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call onNewMessage for debug when debugMode is false', () => {
|
||||
patcher = new ConsolePatcher({
|
||||
onNewMessage,
|
||||
debugMode: false,
|
||||
stderr: false,
|
||||
});
|
||||
patcher.patch();
|
||||
|
||||
console.debug('test debug');
|
||||
expect(onNewMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onNewMessage for debug when debugMode is true', () => {
|
||||
patcher = new ConsolePatcher({
|
||||
onNewMessage,
|
||||
debugMode: true,
|
||||
stderr: false,
|
||||
});
|
||||
patcher.patch();
|
||||
|
||||
console.debug('test debug');
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
type: 'debug',
|
||||
content: 'test debug',
|
||||
count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should format multiple arguments using util.format', () => {
|
||||
patcher = new ConsolePatcher({
|
||||
onNewMessage,
|
||||
debugMode: false,
|
||||
stderr: false,
|
||||
});
|
||||
patcher.patch();
|
||||
|
||||
console.log('test %s %d', 'string', 123);
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
type: 'log',
|
||||
content: 'test string 123',
|
||||
count: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when stderr is true', () => {
|
||||
it('should redirect warn and error to originalConsoleError', () => {
|
||||
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
patcher = new ConsolePatcher({ debugMode: false, stderr: true });
|
||||
patcher.patch();
|
||||
|
||||
console.warn('test warn');
|
||||
expect(spyError).toHaveBeenCalledWith('test warn');
|
||||
|
||||
console.error('test error');
|
||||
expect(spyError).toHaveBeenCalledWith('test error');
|
||||
});
|
||||
|
||||
it('should redirect log and info to originalConsoleError when debugMode is true', () => {
|
||||
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
patcher = new ConsolePatcher({ debugMode: true, stderr: true });
|
||||
patcher.patch();
|
||||
|
||||
console.log('test log');
|
||||
expect(spyError).toHaveBeenCalledWith('test log');
|
||||
|
||||
console.info('test info');
|
||||
expect(spyError).toHaveBeenCalledWith('test info');
|
||||
});
|
||||
|
||||
it('should ignore debug when debugMode is false', () => {
|
||||
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
patcher = new ConsolePatcher({ debugMode: false, stderr: true });
|
||||
patcher.patch();
|
||||
|
||||
console.debug('test debug');
|
||||
expect(spyError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect debug to originalConsoleError when debugMode is true', () => {
|
||||
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
patcher = new ConsolePatcher({ debugMode: true, stderr: true });
|
||||
patcher.patch();
|
||||
|
||||
console.debug('test debug');
|
||||
expect(spyError).toHaveBeenCalledWith('test debug');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,6 @@ interface ConsolePatcherParams {
|
||||
onNewMessage?: (message: Omit<ConsoleMessageItem, 'id'>) => void;
|
||||
debugMode: boolean;
|
||||
stderr?: boolean;
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
export class ConsolePatcher {
|
||||
@@ -50,19 +49,12 @@ export class ConsolePatcher {
|
||||
private patchConsoleMethod =
|
||||
(type: 'log' | 'warn' | 'error' | 'debug' | 'info') =>
|
||||
(...args: unknown[]) => {
|
||||
// When it is non interactive mode, do not show info logging unless
|
||||
// it is debug mode. default to true if it is undefined.
|
||||
if (this.params.interactive === false) {
|
||||
if ((type === 'info' || type === 'log') && !this.params.debugMode) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// When it is in the debug mode, redirect console output to stderr
|
||||
// depending on if it is stderr only mode.
|
||||
if (type !== 'debug' || this.params.debugMode) {
|
||||
if (this.params.stderr) {
|
||||
if (this.params.stderr) {
|
||||
if (type !== 'debug' || this.params.debugMode) {
|
||||
this.originalConsoleError(this.formatArgs(args));
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
if (type !== 'debug' || this.params.debugMode) {
|
||||
this.params.onNewMessage?.({
|
||||
type,
|
||||
content: this.formatArgs(args),
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { styledCharsToString } from '@alcalzone/ansi-tokenize';
|
||||
import {
|
||||
Text,
|
||||
Box,
|
||||
type StyledChar,
|
||||
StyledLine,
|
||||
toStyledCharacters,
|
||||
styledCharsWidth,
|
||||
wordBreakStyledChars,
|
||||
wrapStyledChars,
|
||||
widestLineFromStyledChars,
|
||||
styledCharsWidth,
|
||||
styledCharsToString,
|
||||
} from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { parseMarkdownToANSI } from './markdownParsingUtils.js';
|
||||
@@ -31,22 +31,22 @@ const COLUMN_PADDING = 2;
|
||||
const TABLE_MARGIN = 2;
|
||||
|
||||
/**
|
||||
* Parses markdown to StyledChar array by first converting to ANSI.
|
||||
* Parses markdown to StyledLine by first converting to ANSI.
|
||||
* This ensures character counts are accurate (markdown markers are removed
|
||||
* and styles are applied to the character's internal style object).
|
||||
*/
|
||||
const parseMarkdownToStyledChars = (
|
||||
const parseMarkdownToStyledLine = (
|
||||
text: string,
|
||||
defaultColor?: string,
|
||||
): StyledChar[] => {
|
||||
): StyledLine => {
|
||||
const ansi = parseMarkdownToANSI(text, defaultColor);
|
||||
return toStyledCharacters(ansi);
|
||||
};
|
||||
|
||||
const calculateWidths = (styledChars: StyledChar[]) => {
|
||||
const contentWidth = styledCharsWidth(styledChars);
|
||||
const calculateWidths = (styledLine: StyledLine) => {
|
||||
const contentWidth = styledCharsWidth(styledLine);
|
||||
|
||||
const words: StyledChar[][] = wordBreakStyledChars(styledChars);
|
||||
const words: StyledLine[] = wordBreakStyledChars(styledLine);
|
||||
const maxWordWidth = widestLineFromStyledChars(words);
|
||||
|
||||
return { contentWidth, maxWordWidth };
|
||||
@@ -67,10 +67,10 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
rows,
|
||||
terminalWidth,
|
||||
}) => {
|
||||
const styledHeaders = useMemo(
|
||||
const styledHeaders = useMemo<StyledLine[]>(
|
||||
() =>
|
||||
headers.map((header) =>
|
||||
parseMarkdownToStyledChars(
|
||||
parseMarkdownToStyledLine(
|
||||
stripUnsafeCharacters(header),
|
||||
theme.text.link,
|
||||
),
|
||||
@@ -78,11 +78,11 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
[headers],
|
||||
);
|
||||
|
||||
const styledRows = useMemo(
|
||||
const styledRows = useMemo<StyledLine[][]>(
|
||||
() =>
|
||||
rows.map((row) =>
|
||||
row.map((cell) =>
|
||||
parseMarkdownToStyledChars(
|
||||
parseMarkdownToStyledLine(
|
||||
stripUnsafeCharacters(cell),
|
||||
theme.text.primary,
|
||||
),
|
||||
@@ -100,14 +100,14 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
// --- Define Constraints per Column ---
|
||||
const constraints = Array.from({ length: numColumns }).map(
|
||||
(_, colIndex) => {
|
||||
const headerStyledChars = styledHeaders[colIndex] || [];
|
||||
const headerStyledLine = styledHeaders[colIndex] || StyledLine.empty(0);
|
||||
let { contentWidth: maxContentWidth, maxWordWidth } =
|
||||
calculateWidths(headerStyledChars);
|
||||
calculateWidths(headerStyledLine);
|
||||
|
||||
styledRows.forEach((row) => {
|
||||
const cellStyledChars = row[colIndex] || [];
|
||||
const cellStyledLine = row[colIndex] || StyledLine.empty(0);
|
||||
const { contentWidth: cellWidth, maxWordWidth: cellWordWidth } =
|
||||
calculateWidths(cellStyledChars);
|
||||
calculateWidths(cellStyledLine);
|
||||
|
||||
maxContentWidth = Math.max(maxContentWidth, cellWidth);
|
||||
maxWordWidth = Math.max(maxWordWidth, cellWordWidth);
|
||||
@@ -176,16 +176,16 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
// --- Pre-wrap and Optimize Widths ---
|
||||
const actualColumnWidths = new Array(numColumns).fill(0);
|
||||
|
||||
const wrapAndProcessRow = (row: StyledChar[][]) => {
|
||||
const wrapAndProcessRow = (row: StyledLine[]) => {
|
||||
const rowResult: ProcessedLine[][] = [];
|
||||
// Ensure we iterate up to numColumns, filling with empty cells if needed
|
||||
for (let colIndex = 0; colIndex < numColumns; colIndex++) {
|
||||
const cellStyledChars = row[colIndex] || [];
|
||||
const cellStyledLine = row[colIndex] || StyledLine.empty(0);
|
||||
const allocatedWidth = finalContentWidths[colIndex];
|
||||
const contentWidth = Math.max(1, allocatedWidth);
|
||||
|
||||
const wrappedStyledLines = wrapStyledChars(
|
||||
cellStyledChars,
|
||||
cellStyledLine,
|
||||
contentWidth,
|
||||
);
|
||||
|
||||
|
||||
@@ -14,25 +14,25 @@
|
||||
<text x="243" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="252" lengthAdjust="spacingAndGlyphs">├────────┼────────┼────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">123456</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> 123456 </text>
|
||||
<text x="81" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="99" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Normal</text>
|
||||
<text x="90" y="70" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> Normal </text>
|
||||
<text x="162" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="180" y="70" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">Short</text>
|
||||
<text x="171" y="70" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> Short </text>
|
||||
<text x="243" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">Short</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> Short </text>
|
||||
<text x="81" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="99" y="87" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">123456</text>
|
||||
<text x="90" y="87" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> 123456 </text>
|
||||
<text x="162" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="180" y="87" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Normal</text>
|
||||
<text x="171" y="87" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> Normal </text>
|
||||
<text x="243" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Normal</text>
|
||||
<text x="9" y="104" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> Normal </text>
|
||||
<text x="81" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="99" y="104" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">Short</text>
|
||||
<text x="90" y="104" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> Short </text>
|
||||
<text x="162" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="180" y="104" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">123456</text>
|
||||
<text x="171" y="104" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> 123456 </text>
|
||||
<text x="243" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="252" lengthAdjust="spacingAndGlyphs">└────────┴────────┴────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -14,31 +14,25 @@
|
||||
<text x="918" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="927" lengthAdjust="spacingAndGlyphs">├───────────────────────────────────┼───────────────────────────────┼─────────────────────────────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Visit Google (</text>
|
||||
<text x="144" y="70" fill="#87afff" textLength="162" lengthAdjust="spacingAndGlyphs">https://google.com</text>
|
||||
<text x="306" y="70" fill="#ffffff" textLength="9" lengthAdjust="spacingAndGlyphs">)</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs"> Visit Google (https://google.com) </text>
|
||||
<text x="324" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="342" y="70" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">Plain Text</text>
|
||||
<text x="333" y="70" fill="#ffffff" textLength="279" lengthAdjust="spacingAndGlyphs"> Plain Text </text>
|
||||
<text x="612" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="630" y="70" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs">More Info</text>
|
||||
<text x="621" y="70" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> More Info </text>
|
||||
<text x="918" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs">Info Here</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs"> Info Here </text>
|
||||
<text x="324" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="342" y="87" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Visit Bing (</text>
|
||||
<text x="450" y="87" fill="#87afff" textLength="144" lengthAdjust="spacingAndGlyphs">https://bing.com</text>
|
||||
<text x="594" y="87" fill="#ffffff" textLength="9" lengthAdjust="spacingAndGlyphs">)</text>
|
||||
<text x="333" y="87" fill="#ffffff" textLength="279" lengthAdjust="spacingAndGlyphs"> Visit Bing (https://bing.com) </text>
|
||||
<text x="612" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="630" y="87" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">Links</text>
|
||||
<text x="621" y="87" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> Links </text>
|
||||
<text x="918" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">Check This</text>
|
||||
<text x="9" y="104" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs"> Check This </text>
|
||||
<text x="324" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="342" y="104" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Search</text>
|
||||
<text x="333" y="104" fill="#ffffff" textLength="279" lengthAdjust="spacingAndGlyphs"> Search </text>
|
||||
<text x="612" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="630" y="104" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs">Visit Yahoo (</text>
|
||||
<text x="747" y="104" fill="#87afff" textLength="153" lengthAdjust="spacingAndGlyphs">https://yahoo.com</text>
|
||||
<text x="900" y="104" fill="#ffffff" textLength="9" lengthAdjust="spacingAndGlyphs">)</text>
|
||||
<text x="621" y="104" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> Visit Yahoo (https://yahoo.com) </text>
|
||||
<text x="918" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="927" lengthAdjust="spacingAndGlyphs">└───────────────────────────────────┴───────────────────────────────┴─────────────────────────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -14,26 +14,25 @@
|
||||
<text x="540" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="549" lengthAdjust="spacingAndGlyphs">├─────────────────┼──────────────────────┼──────────────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#d7afff" textLength="108" lengthAdjust="spacingAndGlyphs">**not bold**</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> **not bold** </text>
|
||||
<text x="162" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="180" y="70" fill="#d7afff" textLength="108" lengthAdjust="spacingAndGlyphs">_not italic_</text>
|
||||
<text x="171" y="70" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs"> _not italic_ </text>
|
||||
<text x="369" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="387" y="70" fill="#d7afff" textLength="126" lengthAdjust="spacingAndGlyphs">~~not strike~~</text>
|
||||
<text x="378" y="70" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> ~~not strike~~ </text>
|
||||
<text x="540" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#d7afff" textLength="135" lengthAdjust="spacingAndGlyphs">[not link](url)</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> [not link](url) </text>
|
||||
<text x="162" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="180" y="87" fill="#d7afff" textLength="180" lengthAdjust="spacingAndGlyphs"><u>not underline</u></text>
|
||||
<text x="171" y="87" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs"> <u>not underline</u> </text>
|
||||
<text x="369" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="387" y="87" fill="#d7afff" textLength="144" lengthAdjust="spacingAndGlyphs">https://not.link</text>
|
||||
<text x="378" y="87" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> https://not.link </text>
|
||||
<text x="540" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs">Normal Text</text>
|
||||
<text x="9" y="104" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> Normal Text </text>
|
||||
<text x="162" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="180" y="104" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs">More Code: </text>
|
||||
<text x="279" y="104" fill="#d7afff" textLength="54" lengthAdjust="spacingAndGlyphs">*test*</text>
|
||||
<text x="171" y="104" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs"> More Code: *test* </text>
|
||||
<text x="369" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="387" y="104" fill="#d7afff" textLength="108" lengthAdjust="spacingAndGlyphs">***nested***</text>
|
||||
<text x="378" y="104" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> ***nested*** </text>
|
||||
<text x="540" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="549" lengthAdjust="spacingAndGlyphs">└─────────────────┴──────────────────────┴──────────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
@@ -14,31 +14,25 @@
|
||||
<text x="810" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="819" lengthAdjust="spacingAndGlyphs">├─────────────────────────────┼─────────────────────────────┼─────────────────────────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Bold with </text>
|
||||
<text x="108" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Italic</text>
|
||||
<text x="162" y="70" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold"> and Strike</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Bold with Italic and Strike </text>
|
||||
<text x="270" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Normal</text>
|
||||
<text x="279" y="70" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Normal </text>
|
||||
<text x="540" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="558" y="70" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">Short</text>
|
||||
<text x="549" y="70" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Short </text>
|
||||
<text x="810" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">Short</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Short </text>
|
||||
<text x="270" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="87" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Bold with </text>
|
||||
<text x="378" y="87" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Italic</text>
|
||||
<text x="432" y="87" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold"> and Strike</text>
|
||||
<text x="279" y="87" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Bold with Italic and Strike </text>
|
||||
<text x="540" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="558" y="87" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Normal</text>
|
||||
<text x="549" y="87" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Normal </text>
|
||||
<text x="810" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Normal</text>
|
||||
<text x="9" y="104" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Normal </text>
|
||||
<text x="270" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="104" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">Short</text>
|
||||
<text x="279" y="104" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Short </text>
|
||||
<text x="540" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="558" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Bold with </text>
|
||||
<text x="648" y="104" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Italic</text>
|
||||
<text x="702" y="104" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold"> and Strike</text>
|
||||
<text x="549" y="104" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Bold with Italic and Strike </text>
|
||||
<text x="810" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="819" lengthAdjust="spacingAndGlyphs">└─────────────────────────────┴─────────────────────────────┴─────────────────────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -14,18 +14,18 @@
|
||||
<text x="378" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="405" lengthAdjust="spacingAndGlyphs">├──────────────┼────────────┼───────────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs">Start 🌟 End</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> Start 🌟 End </text>
|
||||
<text x="126" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="144" y="70" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">你好世界</text>
|
||||
<text x="135" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> 你好世界 </text>
|
||||
<text x="243" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="261" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Rocket 🚀 Man</text>
|
||||
<text x="252" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Rocket 🚀 Man </text>
|
||||
<text x="378" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs">Thumbs 👍 Up</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> Thumbs 👍 Up </text>
|
||||
<text x="126" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="144" y="87" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">こんにちは</text>
|
||||
<text x="135" y="87" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> こんにちは </text>
|
||||
<text x="243" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="261" y="87" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Fire 🔥</text>
|
||||
<text x="252" y="87" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Fire 🔥 </text>
|
||||
<text x="378" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="405" lengthAdjust="spacingAndGlyphs">└──────────────┴────────────┴───────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -31,15 +31,15 @@
|
||||
<text x="288" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="297" lengthAdjust="spacingAndGlyphs">├─────────────┼───────┼─────────┤</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="121" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Data 1</text>
|
||||
<text x="9" y="121" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> Data 1 </text>
|
||||
<text x="126" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="144" y="121" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">Data</text>
|
||||
<text x="135" y="121" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs"> Data </text>
|
||||
<text x="198" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="216" y="121" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Data 3</text>
|
||||
<text x="207" y="121" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs"> Data 3 </text>
|
||||
<text x="288" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="126" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="144" y="138" fill="#ffffff" textLength="9" lengthAdjust="spacingAndGlyphs">2</text>
|
||||
<text x="135" y="138" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs"> 2 </text>
|
||||
<text x="198" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#333333" textLength="297" lengthAdjust="spacingAndGlyphs">└─────────────┴───────┴─────────┘</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.6 KiB |
@@ -14,25 +14,25 @@
|
||||
<text x="405" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="414" lengthAdjust="spacingAndGlyphs">├──────────────┼──────────────┼──────────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 1, Col 1</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 1, Col 1 </text>
|
||||
<text x="135" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="153" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 1, Col 2</text>
|
||||
<text x="144" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 1, Col 2 </text>
|
||||
<text x="270" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 1, Col 3</text>
|
||||
<text x="279" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 1, Col 3 </text>
|
||||
<text x="405" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 2, Col 1</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 2, Col 1 </text>
|
||||
<text x="135" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="153" y="87" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 2, Col 2</text>
|
||||
<text x="144" y="87" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 2, Col 2 </text>
|
||||
<text x="270" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="87" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 2, Col 3</text>
|
||||
<text x="279" y="87" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 2, Col 3 </text>
|
||||
<text x="405" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 3, Col 1</text>
|
||||
<text x="9" y="104" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 3, Col 1 </text>
|
||||
<text x="135" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="153" y="104" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 3, Col 2</text>
|
||||
<text x="144" y="104" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 3, Col 2 </text>
|
||||
<text x="270" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="104" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 3, Col 3</text>
|
||||
<text x="279" y="104" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 3, Col 3 </text>
|
||||
<text x="405" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="414" lengthAdjust="spacingAndGlyphs">└──────────────┴──────────────┴──────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -93,105 +93,105 @@
|
||||
<text x="1395" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#333333" textLength="1404" lengthAdjust="spacingAndGlyphs">├─────────────────────────────┼──────────────────────────────┼─────────────────────────────┼──────────────────────────────┼─────┼────────┼─────────┼───────┤</text>
|
||||
<text x="0" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="172" fill="#ffffff" textLength="216" lengthAdjust="spacingAndGlyphs">The primary architecture</text>
|
||||
<text x="9" y="172" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> The primary architecture </text>
|
||||
<text x="270" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="172" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Each message is processed</text>
|
||||
<text x="279" y="172" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> Each message is processed </text>
|
||||
<text x="549" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="172" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Historical data indicates a</text>
|
||||
<text x="558" y="172" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Historical data indicates a </text>
|
||||
<text x="819" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="172" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">A multi-layered defense</text>
|
||||
<text x="828" y="172" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> A multi-layered defense </text>
|
||||
<text x="1098" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1116" y="172" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">INF</text>
|
||||
<text x="1107" y="172" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs"> INF </text>
|
||||
<text x="1152" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1170" y="172" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Active</text>
|
||||
<text x="1161" y="172" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> Active </text>
|
||||
<text x="1233" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1251" y="172" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">v2.4</text>
|
||||
<text x="1242" y="172" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs"> v2.4 </text>
|
||||
<text x="1323" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1341" y="172" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">J.</text>
|
||||
<text x="1332" y="172" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs"> J. </text>
|
||||
<text x="1395" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="189" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">utilizes a decoupled</text>
|
||||
<text x="9" y="189" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> utilizes a decoupled </text>
|
||||
<text x="270" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="189" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs">through a series of</text>
|
||||
<text x="279" y="189" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> through a series of </text>
|
||||
<text x="549" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="189" fill="#ffffff" textLength="216" lengthAdjust="spacingAndGlyphs">significant reduction in</text>
|
||||
<text x="558" y="189" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> significant reduction in </text>
|
||||
<text x="819" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="189" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs">strategy incorporates</text>
|
||||
<text x="828" y="189" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> strategy incorporates </text>
|
||||
<text x="1098" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1341" y="189" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">Doe</text>
|
||||
<text x="1332" y="189" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs"> Doe </text>
|
||||
<text x="1395" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="206" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">microservices approach,</text>
|
||||
<text x="9" y="206" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> microservices approach, </text>
|
||||
<text x="270" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="206" fill="#ffffff" textLength="216" lengthAdjust="spacingAndGlyphs">specialized workers that</text>
|
||||
<text x="279" y="206" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> specialized workers that </text>
|
||||
<text x="549" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="206" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">tail latency when utilizing</text>
|
||||
<text x="558" y="206" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> tail latency when utilizing </text>
|
||||
<text x="819" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="206" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs">content security policies,</text>
|
||||
<text x="828" y="206" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> content security policies, </text>
|
||||
<text x="1098" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="223" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">leveraging container</text>
|
||||
<text x="9" y="223" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> leveraging container </text>
|
||||
<text x="270" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="223" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">handle data transformation,</text>
|
||||
<text x="279" y="223" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> handle data transformation, </text>
|
||||
<text x="549" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="223" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">edge computing nodes closer</text>
|
||||
<text x="558" y="223" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> edge computing nodes closer </text>
|
||||
<text x="819" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="223" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">input sanitization</text>
|
||||
<text x="828" y="223" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> input sanitization </text>
|
||||
<text x="1098" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="240" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs">orchestration for</text>
|
||||
<text x="9" y="240" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> orchestration for </text>
|
||||
<text x="270" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="240" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs">validation, and persistent</text>
|
||||
<text x="279" y="240" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> validation, and persistent </text>
|
||||
<text x="549" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="240" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs">to the geographic location</text>
|
||||
<text x="558" y="240" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> to the geographic location </text>
|
||||
<text x="819" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="240" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">libraries, and regular</text>
|
||||
<text x="828" y="240" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> libraries, and regular </text>
|
||||
<text x="1098" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="257" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs">scalability and fault</text>
|
||||
<text x="9" y="257" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> scalability and fault </text>
|
||||
<text x="270" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="257" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs">storage using a persistent</text>
|
||||
<text x="279" y="257" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> storage using a persistent </text>
|
||||
<text x="549" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="257" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs">of the end-user base.</text>
|
||||
<text x="558" y="257" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> of the end-user base. </text>
|
||||
<text x="819" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="257" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs">automated penetration</text>
|
||||
<text x="828" y="257" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> automated penetration </text>
|
||||
<text x="1098" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="274" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">tolerance in high-load</text>
|
||||
<text x="9" y="274" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> tolerance in high-load </text>
|
||||
<text x="270" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="274" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">queue.</text>
|
||||
<text x="279" y="274" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> queue. </text>
|
||||
<text x="549" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="819" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="274" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs">testing routines.</text>
|
||||
<text x="828" y="274" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> testing routines. </text>
|
||||
<text x="1098" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="291" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">scenarios.</text>
|
||||
<text x="9" y="291" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> scenarios. </text>
|
||||
<text x="270" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="549" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="291" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs">Monitoring tools have</text>
|
||||
<text x="558" y="291" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Monitoring tools have </text>
|
||||
<text x="819" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1098" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -200,85 +200,85 @@
|
||||
<text x="1395" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="270" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="308" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs">The pipeline features</text>
|
||||
<text x="279" y="308" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> The pipeline features </text>
|
||||
<text x="549" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="308" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs">captured a steady increase</text>
|
||||
<text x="558" y="308" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> captured a steady increase </text>
|
||||
<text x="819" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="308" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs">Developers are required to</text>
|
||||
<text x="828" y="308" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> Developers are required to </text>
|
||||
<text x="1098" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="325" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">This layer provides the</text>
|
||||
<text x="9" y="325" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> This layer provides the </text>
|
||||
<text x="270" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="325" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">built-in retry mechanisms</text>
|
||||
<text x="279" y="325" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> built-in retry mechanisms </text>
|
||||
<text x="549" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="325" fill="#ffffff" textLength="216" lengthAdjust="spacingAndGlyphs">in throughput efficiency</text>
|
||||
<text x="558" y="325" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> in throughput efficiency </text>
|
||||
<text x="819" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="325" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs">undergo mandatory security</text>
|
||||
<text x="828" y="325" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> undergo mandatory security </text>
|
||||
<text x="1098" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="342" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">fundamental building blocks</text>
|
||||
<text x="9" y="342" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> fundamental building blocks </text>
|
||||
<text x="270" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="342" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">with exponential backoff to</text>
|
||||
<text x="279" y="342" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> with exponential backoff to </text>
|
||||
<text x="549" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="342" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">since the introduction of</text>
|
||||
<text x="558" y="342" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> since the introduction of </text>
|
||||
<text x="819" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="342" fill="#ffffff" textLength="216" lengthAdjust="spacingAndGlyphs">training focusing on the</text>
|
||||
<text x="828" y="342" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> training focusing on the </text>
|
||||
<text x="1098" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="359" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">for service discovery, load</text>
|
||||
<text x="9" y="359" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> for service discovery, load </text>
|
||||
<text x="270" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="359" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">ensure message delivery</text>
|
||||
<text x="279" y="359" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> ensure message delivery </text>
|
||||
<text x="549" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="359" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">the vectorized query engine</text>
|
||||
<text x="558" y="359" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> the vectorized query engine </text>
|
||||
<text x="819" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">OWASP Top Ten to ensure that</text>
|
||||
<text x="828" y="359" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> OWASP Top Ten to ensure that </text>
|
||||
<text x="1098" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="376" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">balancing, and</text>
|
||||
<text x="9" y="376" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> balancing, and </text>
|
||||
<text x="270" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="376" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs">integrity even during</text>
|
||||
<text x="279" y="376" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> integrity even during </text>
|
||||
<text x="549" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="376" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs">in the primary data</text>
|
||||
<text x="558" y="376" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> in the primary data </text>
|
||||
<text x="819" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="376" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">security is integrated into</text>
|
||||
<text x="828" y="376" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> security is integrated into </text>
|
||||
<text x="1098" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="393" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">inter-service communication</text>
|
||||
<text x="9" y="393" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> inter-service communication </text>
|
||||
<text x="270" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="393" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">transient network or service</text>
|
||||
<text x="279" y="393" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> transient network or service </text>
|
||||
<text x="549" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="393" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">warehouse.</text>
|
||||
<text x="558" y="393" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> warehouse. </text>
|
||||
<text x="819" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="393" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">the initial design phase.</text>
|
||||
<text x="828" y="393" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> the initial design phase. </text>
|
||||
<text x="1098" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="410" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">via highly efficient</text>
|
||||
<text x="9" y="410" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> via highly efficient </text>
|
||||
<text x="270" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="410" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs">failures.</text>
|
||||
<text x="279" y="410" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> failures. </text>
|
||||
<text x="549" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="819" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1098" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -287,12 +287,12 @@
|
||||
<text x="1323" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="427" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs">protocol buffers.</text>
|
||||
<text x="9" y="427" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> protocol buffers. </text>
|
||||
<text x="270" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="549" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="427" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">Resource utilization</text>
|
||||
<text x="558" y="427" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Resource utilization </text>
|
||||
<text x="819" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="427" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">The implementation of a</text>
|
||||
<text x="828" y="427" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> The implementation of a </text>
|
||||
<text x="1098" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -300,85 +300,85 @@
|
||||
<text x="1395" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="270" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="444" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Horizontal autoscaling is</text>
|
||||
<text x="279" y="444" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> Horizontal autoscaling is </text>
|
||||
<text x="549" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="444" fill="#ffffff" textLength="216" lengthAdjust="spacingAndGlyphs">metrics demonstrate that</text>
|
||||
<text x="558" y="444" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> metrics demonstrate that </text>
|
||||
<text x="819" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="444" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs">robust Identity and Access</text>
|
||||
<text x="828" y="444" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> robust Identity and Access </text>
|
||||
<text x="1098" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="461" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Advanced telemetry and</text>
|
||||
<text x="9" y="461" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Advanced telemetry and </text>
|
||||
<text x="270" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="461" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">triggered automatically</text>
|
||||
<text x="279" y="461" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> triggered automatically </text>
|
||||
<text x="549" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="461" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs">the transition to</text>
|
||||
<text x="558" y="461" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> the transition to </text>
|
||||
<text x="819" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="461" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Management system ensures</text>
|
||||
<text x="828" y="461" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> Management system ensures </text>
|
||||
<text x="1098" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="478" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs">logging integrations allow</text>
|
||||
<text x="9" y="478" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> logging integrations allow </text>
|
||||
<text x="270" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="478" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">based on the depth of the</text>
|
||||
<text x="279" y="478" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> based on the depth of the </text>
|
||||
<text x="549" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="478" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">serverless compute for</text>
|
||||
<text x="558" y="478" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> serverless compute for </text>
|
||||
<text x="819" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="478" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">that the principle of least</text>
|
||||
<text x="828" y="478" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> that the principle of least </text>
|
||||
<text x="1098" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="495" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">for real-time monitoring of</text>
|
||||
<text x="9" y="495" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> for real-time monitoring of </text>
|
||||
<text x="270" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="495" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs">processing queue, ensuring</text>
|
||||
<text x="279" y="495" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> processing queue, ensuring </text>
|
||||
<text x="549" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="495" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">intermittent tasks has</text>
|
||||
<text x="558" y="495" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> intermittent tasks has </text>
|
||||
<text x="819" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="495" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs">privilege is strictly</text>
|
||||
<text x="828" y="495" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> privilege is strictly </text>
|
||||
<text x="1098" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="512" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">system health and rapid</text>
|
||||
<text x="9" y="512" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> system health and rapid </text>
|
||||
<text x="270" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="512" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">consistent performance</text>
|
||||
<text x="279" y="512" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> consistent performance </text>
|
||||
<text x="549" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="512" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">resulted in a thirty</text>
|
||||
<text x="558" y="512" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> resulted in a thirty </text>
|
||||
<text x="819" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="512" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs">enforced across all</text>
|
||||
<text x="828" y="512" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> enforced across all </text>
|
||||
<text x="1098" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="529" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs">identification of</text>
|
||||
<text x="9" y="529" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> identification of </text>
|
||||
<text x="270" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="529" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">during unexpected traffic</text>
|
||||
<text x="279" y="529" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> during unexpected traffic </text>
|
||||
<text x="549" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="567" y="529" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs">percent cost optimization.</text>
|
||||
<text x="558" y="529" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> percent cost optimization. </text>
|
||||
<text x="819" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="837" y="529" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs">environments.</text>
|
||||
<text x="828" y="529" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> environments. </text>
|
||||
<text x="1098" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="546" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">bottlenecks within the</text>
|
||||
<text x="9" y="546" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> bottlenecks within the </text>
|
||||
<text x="270" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="546" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">spikes.</text>
|
||||
<text x="279" y="546" fill="#ffffff" textLength="270" lengthAdjust="spacingAndGlyphs"> spikes. </text>
|
||||
<text x="549" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="819" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1098" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -387,7 +387,7 @@
|
||||
<text x="1323" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1395" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="563" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs">service mesh.</text>
|
||||
<text x="9" y="563" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> service mesh. </text>
|
||||
<text x="270" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="549" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="819" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
@@ -32,31 +32,31 @@
|
||||
<text x="630" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="639" lengthAdjust="spacingAndGlyphs">├───────────────┼───────────────┼──────────────────┼──────────────────┤</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 1.1</text>
|
||||
<text x="9" y="104" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Data 1.1 </text>
|
||||
<text x="144" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="162" y="104" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 1.2</text>
|
||||
<text x="153" y="104" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Data 1.2 </text>
|
||||
<text x="288" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="306" y="104" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 1.3</text>
|
||||
<text x="297" y="104" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> Data 1.3 </text>
|
||||
<text x="459" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="477" y="104" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 1.4</text>
|
||||
<text x="468" y="104" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> Data 1.4 </text>
|
||||
<text x="630" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="121" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 2.1</text>
|
||||
<text x="9" y="121" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Data 2.1 </text>
|
||||
<text x="144" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="162" y="121" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 2.2</text>
|
||||
<text x="153" y="121" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Data 2.2 </text>
|
||||
<text x="288" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="306" y="121" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 2.3</text>
|
||||
<text x="297" y="121" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> Data 2.3 </text>
|
||||
<text x="459" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="477" y="121" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 2.4</text>
|
||||
<text x="468" y="121" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> Data 2.4 </text>
|
||||
<text x="630" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="138" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 3.1</text>
|
||||
<text x="9" y="138" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Data 3.1 </text>
|
||||
<text x="144" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="162" y="138" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 3.2</text>
|
||||
<text x="153" y="138" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Data 3.2 </text>
|
||||
<text x="288" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="306" y="138" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 3.3</text>
|
||||
<text x="297" y="138" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> Data 3.3 </text>
|
||||
<text x="459" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="477" y="138" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Data 3.4</text>
|
||||
<text x="468" y="138" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> Data 3.4 </text>
|
||||
<text x="630" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#333333" textLength="639" lengthAdjust="spacingAndGlyphs">└───────────────┴───────────────┴──────────────────┴──────────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -14,18 +14,18 @@
|
||||
<text x="450" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="486" lengthAdjust="spacingAndGlyphs">├───────────────┼───────────────────┼────────────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">你好 😃</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> 你好 😃 </text>
|
||||
<text x="135" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="153" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">こんにちは 🚀</text>
|
||||
<text x="144" y="70" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> こんにちは 🚀 </text>
|
||||
<text x="306" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="324" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">안녕하세요 📝</text>
|
||||
<text x="315" y="70" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> 안녕하세요 📝 </text>
|
||||
<text x="450" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">World 🌍</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> World 🌍 </text>
|
||||
<text x="135" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="153" y="87" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Code 💻</text>
|
||||
<text x="144" y="87" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> Code 💻 </text>
|
||||
<text x="306" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="324" y="87" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">Pizza 🍕</text>
|
||||
<text x="315" y="87" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Pizza 🍕 </text>
|
||||
<text x="450" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="486" lengthAdjust="spacingAndGlyphs">└───────────────┴───────────────────┴────────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -14,18 +14,18 @@
|
||||
<text x="441" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="450" lengthAdjust="spacingAndGlyphs">├──────────────┼─────────────────┼───────────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">你好</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> 你好 </text>
|
||||
<text x="135" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="153" y="70" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">こんにちは</text>
|
||||
<text x="144" y="70" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> こんにちは </text>
|
||||
<text x="297" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="315" y="70" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">안녕하세요</text>
|
||||
<text x="306" y="70" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> 안녕하세요 </text>
|
||||
<text x="441" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">世界</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> 世界 </text>
|
||||
<text x="135" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="153" y="87" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">世界</text>
|
||||
<text x="144" y="87" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> 世界 </text>
|
||||
<text x="297" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="315" y="87" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">세계</text>
|
||||
<text x="306" y="87" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> 세계 </text>
|
||||
<text x="441" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="450" lengthAdjust="spacingAndGlyphs">└──────────────┴─────────────────┴───────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -14,18 +14,18 @@
|
||||
<text x="279" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="315" lengthAdjust="spacingAndGlyphs">├──────────┼───────────┼──────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">Smile 😃</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs"> Smile 😃 </text>
|
||||
<text x="90" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="108" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Fire 🔥</text>
|
||||
<text x="99" y="70" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs"> Fire 🔥 </text>
|
||||
<text x="189" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="207" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Love 💖</text>
|
||||
<text x="198" y="70" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs"> Love 💖 </text>
|
||||
<text x="279" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Cool 😎</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs"> Cool 😎 </text>
|
||||
<text x="90" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="108" y="87" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Star ⭐</text>
|
||||
<text x="99" y="87" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs"> Star ⭐ </text>
|
||||
<text x="189" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="207" y="87" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Blue 💙</text>
|
||||
<text x="198" y="87" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs"> Blue 💙 </text>
|
||||
<text x="279" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="315" lengthAdjust="spacingAndGlyphs">└──────────┴───────────┴──────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.1 KiB |
@@ -12,41 +12,39 @@
|
||||
<text x="414" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="423" lengthAdjust="spacingAndGlyphs">├───────────────┼─────────────────────────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">Bold</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Bold </text>
|
||||
<text x="144" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="162" y="70" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">Bold Text</text>
|
||||
<text x="153" y="70" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Bold Text </text>
|
||||
<text x="414" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Italic</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Italic </text>
|
||||
<text x="144" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="162" y="87" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-style="italic">Italic Text</text>
|
||||
<text x="153" y="87" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Italic Text </text>
|
||||
<text x="414" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Combined</text>
|
||||
<text x="9" y="104" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Combined </text>
|
||||
<text x="144" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="162" y="104" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Bold and Italic</text>
|
||||
<text x="153" y="104" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Bold and Italic </text>
|
||||
<text x="414" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="121" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">Link</text>
|
||||
<text x="9" y="121" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Link </text>
|
||||
<text x="144" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="162" y="121" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">Google (</text>
|
||||
<text x="234" y="121" fill="#87afff" textLength="162" lengthAdjust="spacingAndGlyphs">https://google.com</text>
|
||||
<text x="396" y="121" fill="#ffffff" textLength="9" lengthAdjust="spacingAndGlyphs">)</text>
|
||||
<text x="153" y="121" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Google (https://google.com) </text>
|
||||
<text x="414" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="138" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">Code</text>
|
||||
<text x="9" y="138" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Code </text>
|
||||
<text x="144" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="162" y="138" fill="#d7afff" textLength="99" lengthAdjust="spacingAndGlyphs">const x = 1</text>
|
||||
<text x="153" y="138" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> const x = 1 </text>
|
||||
<text x="414" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="155" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs">Strikethrough</text>
|
||||
<text x="9" y="155" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Strikethrough </text>
|
||||
<text x="144" y="155" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="162" y="155" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Strike</text>
|
||||
<text x="153" y="155" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Strike </text>
|
||||
<text x="414" y="155" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="172" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs">Underline</text>
|
||||
<text x="9" y="172" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Underline </text>
|
||||
<text x="144" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="162" y="172" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" text-decoration="underline">Underline</text>
|
||||
<text x="153" y="172" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> Underline </text>
|
||||
<text x="414" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#333333" textLength="423" lengthAdjust="spacingAndGlyphs">└───────────────┴─────────────────────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -10,9 +10,9 @@
|
||||
<text x="162" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="171" lengthAdjust="spacingAndGlyphs">├────────┼────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Data 1</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> Data 1 </text>
|
||||
<text x="81" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="99" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Data 2</text>
|
||||
<text x="90" y="70" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> Data 2 </text>
|
||||
<text x="162" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="171" lengthAdjust="spacingAndGlyphs">└────────┴────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -14,9 +14,9 @@
|
||||
<text x="297" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="306" lengthAdjust="spacingAndGlyphs">├──────────┼──────────┼──────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Data 1</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs"> Data 1 </text>
|
||||
<text x="99" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="117" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Data 2</text>
|
||||
<text x="108" y="70" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs"> Data 2 </text>
|
||||
<text x="198" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="297" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="306" lengthAdjust="spacingAndGlyphs">└──────────┴──────────┴──────────┘</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -14,11 +14,11 @@
|
||||
<text x="405" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="414" lengthAdjust="spacingAndGlyphs">├─────────────┼───────────────┼──────────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Data 1</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> Data 1 </text>
|
||||
<text x="126" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="144" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Data 2</text>
|
||||
<text x="135" y="70" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Data 2 </text>
|
||||
<text x="270" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Data 3</text>
|
||||
<text x="279" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Data 3 </text>
|
||||
<text x="405" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="414" lengthAdjust="spacingAndGlyphs">└─────────────┴───────────────┴──────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -14,38 +14,38 @@
|
||||
<text x="468" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="477" lengthAdjust="spacingAndGlyphs">├────────────────┼────────────────┼─────────────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">This is a very</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs"> This is a very </text>
|
||||
<text x="153" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="171" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">This is also a</text>
|
||||
<text x="162" y="70" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs"> This is also a </text>
|
||||
<text x="306" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="324" y="70" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs">And this is the</text>
|
||||
<text x="315" y="70" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> And this is the </text>
|
||||
<text x="468" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">long text that</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs"> long text that </text>
|
||||
<text x="153" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="171" y="87" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">very long text</text>
|
||||
<text x="162" y="87" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs"> very long text </text>
|
||||
<text x="306" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="324" y="87" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs">third long text</text>
|
||||
<text x="315" y="87" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> third long text </text>
|
||||
<text x="468" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">needs wrapping</text>
|
||||
<text x="9" y="104" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs"> needs wrapping </text>
|
||||
<text x="153" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="171" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">that needs</text>
|
||||
<text x="162" y="104" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs"> that needs </text>
|
||||
<text x="306" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="324" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">that needs</text>
|
||||
<text x="315" y="104" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> that needs </text>
|
||||
<text x="468" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="121" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs">in column 1</text>
|
||||
<text x="9" y="121" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs"> in column 1 </text>
|
||||
<text x="153" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="171" y="121" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs">wrapping in</text>
|
||||
<text x="162" y="121" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs"> wrapping in </text>
|
||||
<text x="306" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="324" y="121" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs">wrapping in</text>
|
||||
<text x="315" y="121" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> wrapping in </text>
|
||||
<text x="468" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="153" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="171" y="138" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">column 2</text>
|
||||
<text x="162" y="138" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs"> column 2 </text>
|
||||
<text x="306" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="324" y="138" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">column 3</text>
|
||||
<text x="315" y="138" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> column 3 </text>
|
||||
<text x="468" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#333333" textLength="477" lengthAdjust="spacingAndGlyphs">└────────────────┴────────────────┴─────────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -14,37 +14,37 @@
|
||||
<text x="486" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="495" lengthAdjust="spacingAndGlyphs">├───────────────────┼───────────────┼─────────────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Start. Stop.</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Start. Stop. </text>
|
||||
<text x="180" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="198" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Semi; colon:</text>
|
||||
<text x="189" y="70" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Semi; colon: </text>
|
||||
<text x="324" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="342" y="70" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs">At@ Hash#</text>
|
||||
<text x="333" y="70" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> At@ Hash# </text>
|
||||
<text x="486" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs">Comma, separated.</text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Comma, separated. </text>
|
||||
<text x="180" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="198" y="87" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Pipe| Slash/</text>
|
||||
<text x="189" y="87" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Pipe| Slash/ </text>
|
||||
<text x="324" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="342" y="87" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">Dollar$</text>
|
||||
<text x="333" y="87" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> Dollar$ </text>
|
||||
<text x="486" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Exclamation!</text>
|
||||
<text x="9" y="104" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Exclamation! </text>
|
||||
<text x="180" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="198" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">Backslash\</text>
|
||||
<text x="189" y="104" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Backslash\ </text>
|
||||
<text x="324" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="342" y="104" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs">Percent% Caret^</text>
|
||||
<text x="333" y="104" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> Percent% Caret^ </text>
|
||||
<text x="486" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="121" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs">Question?</text>
|
||||
<text x="9" y="121" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Question? </text>
|
||||
<text x="180" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="324" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="342" y="121" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">Ampersand&</text>
|
||||
<text x="333" y="121" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> Ampersand& </text>
|
||||
<text x="486" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="138" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs">hyphen-ated</text>
|
||||
<text x="9" y="138" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> hyphen-ated </text>
|
||||
<text x="180" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="324" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="342" y="138" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs">Asterisk*</text>
|
||||
<text x="333" y="138" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs"> Asterisk* </text>
|
||||
<text x="486" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#333333" textLength="495" lengthAdjust="spacingAndGlyphs">└───────────────────┴───────────────┴─────────────────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -14,20 +14,20 @@
|
||||
<text x="414" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="423" lengthAdjust="spacingAndGlyphs">├───────┼─────────────────────────────┼───────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">Short</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs"> Short </text>
|
||||
<text x="72" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="90" y="70" fill="#ffffff" textLength="216" lengthAdjust="spacingAndGlyphs">This is a very long cell</text>
|
||||
<text x="81" y="70" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> This is a very long cell </text>
|
||||
<text x="342" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="360" y="70" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">Short</text>
|
||||
<text x="351" y="70" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs"> Short </text>
|
||||
<text x="414" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="72" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="90" y="87" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">content that should wrap to</text>
|
||||
<text x="81" y="87" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> content that should wrap to </text>
|
||||
<text x="342" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="414" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="72" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="90" y="104" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">multiple lines</text>
|
||||
<text x="81" y="104" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs"> multiple lines </text>
|
||||
<text x="342" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="414" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="423" lengthAdjust="spacingAndGlyphs">└───────┴─────────────────────────────┴───────┘</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -14,21 +14,21 @@
|
||||
<text x="396" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="405" lengthAdjust="spacingAndGlyphs">├───────┼──────────────────────────┼────────┤</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">Tiny</text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs"> Tiny </text>
|
||||
<text x="72" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="90" y="70" fill="#ffffff" textLength="216" lengthAdjust="spacingAndGlyphs">This is a very long text</text>
|
||||
<text x="81" y="70" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs"> This is a very long text </text>
|
||||
<text x="315" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="333" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Not so</text>
|
||||
<text x="324" y="70" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> Not so </text>
|
||||
<text x="396" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="72" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="90" y="87" fill="#ffffff" textLength="216" lengthAdjust="spacingAndGlyphs">that definitely needs to</text>
|
||||
<text x="81" y="87" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs"> that definitely needs to </text>
|
||||
<text x="315" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="333" y="87" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">long</text>
|
||||
<text x="324" y="87" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> long </text>
|
||||
<text x="396" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="72" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="90" y="104" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs">wrap to the next line</text>
|
||||
<text x="81" y="104" fill="#ffffff" textLength="234" lengthAdjust="spacingAndGlyphs"> wrap to the next line </text>
|
||||
<text x="315" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="396" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="405" lengthAdjust="spacingAndGlyphs">└───────┴──────────────────────────┴────────┘</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -27,7 +27,6 @@ export interface UpdateInfo {
|
||||
export interface UpdateObject {
|
||||
message: string;
|
||||
update: UpdateInfo;
|
||||
isUpdating?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -197,9 +197,7 @@ describe('handleAutoUpdate', () => {
|
||||
|
||||
expect(updateEventEmitter.emit).toHaveBeenCalledTimes(1);
|
||||
expect(updateEventEmitter.emit).toHaveBeenCalledWith('update-received', {
|
||||
...mockUpdateInfo,
|
||||
message: 'An update is available!\nPlease update manually.',
|
||||
isUpdating: false,
|
||||
});
|
||||
expect(mockSpawn).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -238,9 +236,7 @@ describe('handleAutoUpdate', () => {
|
||||
|
||||
expect(updateEventEmitter.emit).toHaveBeenCalledTimes(1);
|
||||
expect(updateEventEmitter.emit).toHaveBeenCalledWith('update-received', {
|
||||
...mockUpdateInfo,
|
||||
message: 'An update is available!\nCannot determine update command.',
|
||||
isUpdating: false,
|
||||
});
|
||||
expect(mockSpawn).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -257,9 +253,7 @@ describe('handleAutoUpdate', () => {
|
||||
|
||||
expect(updateEventEmitter.emit).toHaveBeenCalledTimes(1);
|
||||
expect(updateEventEmitter.emit).toHaveBeenCalledWith('update-received', {
|
||||
...mockUpdateInfo,
|
||||
message: 'An update is available!\nThis is an additional message.',
|
||||
isUpdating: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||