Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8d4d944e8 | |||
| 358ca8092a | |||
| 69028570ed | |||
| f593928cd6 |
@@ -31,7 +31,6 @@ jobs:
|
||||
name: 'Merge Queue Skipper'
|
||||
permissions: 'read-all'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
outputs:
|
||||
skip: '${{ steps.merge-queue-e2e-skipper.outputs.skip-check }}'
|
||||
steps:
|
||||
@@ -43,7 +42,7 @@ jobs:
|
||||
|
||||
download_repo_name:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run')"
|
||||
if: "${{github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run'}}"
|
||||
outputs:
|
||||
repo_name: '${{ steps.output-repo-name.outputs.repo_name }}'
|
||||
head_sha: '${{ steps.output-repo-name.outputs.head_sha }}'
|
||||
@@ -54,7 +53,7 @@ jobs:
|
||||
REPO_NAME: '${{ github.event.inputs.repo_name }}'
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo "${REPO_NAME}" > ./pr/repo_name
|
||||
echo '${REPO_NAME}' > ./pr/repo_name
|
||||
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'repo_name'
|
||||
@@ -92,7 +91,7 @@ jobs:
|
||||
name: 'Parse run context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs: 'download_repo_name'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
outputs:
|
||||
repository: '${{ steps.set_context.outputs.REPO }}'
|
||||
sha: '${{ steps.set_context.outputs.SHA }}'
|
||||
@@ -112,11 +111,11 @@ jobs:
|
||||
permissions: 'write-all'
|
||||
needs:
|
||||
- 'parse_run_context'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
steps:
|
||||
- name: 'Set pending status'
|
||||
uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
with:
|
||||
allowForks: 'true'
|
||||
repo: '${{ github.repository }}'
|
||||
@@ -132,7 +131,7 @@ jobs:
|
||||
- 'parse_run_context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -185,7 +184,7 @@ jobs:
|
||||
- 'parse_run_context'
|
||||
runs-on: 'macos-latest'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
@@ -223,7 +222,7 @@ jobs:
|
||||
- 'merge_queue_skipper'
|
||||
- 'parse_run_context'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
@@ -283,7 +282,7 @@ jobs:
|
||||
- 'parse_run_context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
@@ -310,7 +309,7 @@ jobs:
|
||||
e2e:
|
||||
name: 'E2E'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
needs:
|
||||
- 'e2e_linux'
|
||||
- 'e2e_mac'
|
||||
@@ -338,14 +337,14 @@ jobs:
|
||||
set_workflow_status:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions: 'write-all'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
needs:
|
||||
- 'parse_run_context'
|
||||
- 'e2e'
|
||||
steps:
|
||||
- name: 'Set workflow status'
|
||||
uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
with:
|
||||
allowForks: 'true'
|
||||
repo: '${{ github.repository }}'
|
||||
|
||||
@@ -37,7 +37,6 @@ jobs:
|
||||
permissions: 'read-all'
|
||||
name: 'Merge Queue Skipper'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
outputs:
|
||||
skip: '${{ steps.merge-queue-ci-skipper.outputs.skip-check }}'
|
||||
steps:
|
||||
@@ -50,7 +49,7 @@ jobs:
|
||||
name: 'Lint'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
env:
|
||||
GEMINI_LINT_TEMP_DIR: '${{ github.workspace }}/.gemini-linters'
|
||||
steps:
|
||||
@@ -117,7 +116,6 @@ jobs:
|
||||
link_checker:
|
||||
name: 'Link Checker'
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
@@ -131,7 +129,7 @@ jobs:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
permissions:
|
||||
contents: 'read'
|
||||
checks: 'write'
|
||||
@@ -218,7 +216,7 @@ jobs:
|
||||
runs-on: 'macos-latest'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
permissions:
|
||||
contents: 'read'
|
||||
checks: 'write'
|
||||
@@ -313,7 +311,7 @@ jobs:
|
||||
name: 'CodeQL'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
permissions:
|
||||
actions: 'read'
|
||||
contents: 'read'
|
||||
@@ -336,7 +334,7 @@ jobs:
|
||||
bundle_size:
|
||||
name: 'Check Bundle Size'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && github.event_name == 'pull_request' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{github.event_name == 'pull_request' && needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions:
|
||||
contents: 'read' # For checkout
|
||||
@@ -361,7 +359,7 @@ jobs:
|
||||
name: 'Slow Test - Win - ${{ matrix.shard }}'
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -453,7 +451,7 @@ jobs:
|
||||
|
||||
ci:
|
||||
name: 'CI'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
needs:
|
||||
- 'lint'
|
||||
- 'link_checker'
|
||||
|
||||
@@ -27,7 +27,6 @@ jobs:
|
||||
deflake_e2e_linux:
|
||||
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -78,7 +77,6 @@ jobs:
|
||||
deflake_e2e_mac:
|
||||
name: 'E2E Test (macOS)'
|
||||
runs-on: 'macos-latest'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
@@ -116,7 +114,6 @@ jobs:
|
||||
deflake_e2e_windows:
|
||||
name: 'Slow E2E - Win'
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: 'Deploy GitHub Pages'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
pages: 'write'
|
||||
id-token: 'write'
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run
|
||||
# in-progress and latest queued. However, do NOT cancel in-progress runs as we
|
||||
# want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}'
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: |-
|
||||
${{ !contains(github.ref_name, 'nightly') }}
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
|
||||
- name: 'Setup Pages'
|
||||
uses: 'actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b' # ratchet:actions/configure-pages@v5
|
||||
|
||||
- name: 'Build with Jekyll'
|
||||
uses: 'actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697' # ratchet:actions/jekyll-build-pages@v1
|
||||
with:
|
||||
source: './'
|
||||
destination: './_site'
|
||||
|
||||
- name: 'Upload artifact'
|
||||
uses: 'actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa' # ratchet:actions/upload-pages-artifact@v3
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: 'github-pages'
|
||||
url: '${{ steps.deployment.outputs.page_url }}'
|
||||
runs-on: 'ubuntu-latest'
|
||||
needs: 'build'
|
||||
steps:
|
||||
- name: 'Deploy to GitHub Pages'
|
||||
id: 'deployment'
|
||||
uses: 'actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e' # ratchet:actions/deploy-pages@v4
|
||||
@@ -7,7 +7,6 @@ on:
|
||||
- 'docs/**'
|
||||
jobs:
|
||||
trigger-rebuild:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Trigger rebuild'
|
||||
|
||||
@@ -23,7 +23,6 @@ jobs:
|
||||
evals:
|
||||
name: 'Evals (USUALLY_PASSING) nightly run'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -86,7 +85,7 @@ jobs:
|
||||
aggregate-results:
|
||||
name: 'Aggregate Results'
|
||||
needs: ['evals']
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
|
||||
@@ -21,7 +21,6 @@ defaults:
|
||||
|
||||
jobs:
|
||||
close-stale-issues:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
issues: 'write'
|
||||
|
||||
@@ -14,7 +14,7 @@ permissions:
|
||||
jobs:
|
||||
# Event-based: Quick reaction to new/edited issues in THIS repo
|
||||
labeler:
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && github.event_name == 'issues'"
|
||||
if: "github.event_name == 'issues'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
# Scheduled/Manual: Recursive sync across multiple repos
|
||||
sync-maintainer-labels:
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')"
|
||||
if: "github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
|
||||
@@ -9,7 +9,6 @@ on:
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
issues: 'write'
|
||||
|
||||
@@ -32,7 +32,6 @@ on:
|
||||
|
||||
jobs:
|
||||
change-tags:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
|
||||
@@ -47,7 +47,6 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
|
||||
@@ -22,7 +22,6 @@ on:
|
||||
|
||||
jobs:
|
||||
generate-release-notes:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
|
||||
@@ -42,7 +42,6 @@ on:
|
||||
|
||||
jobs:
|
||||
change-tags:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
@@ -204,7 +203,7 @@ jobs:
|
||||
run: |
|
||||
ROLLBACK_COMMIT=$(git rev-parse -q --verify "$TARGET_TAG")
|
||||
if [ "$ROLLBACK_COMMIT" != "$TARGET_HASH" ]; then
|
||||
echo "❌ Failed to add tag ${TARGET_TAG} to commit ${TARGET_HASH}"
|
||||
echo '❌ Failed to add tag $TARGET_TAG to commit $TARGET_HASH'
|
||||
echo '❌ This means the tag was not added, and the workflow should fail.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -16,7 +16,6 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
@@ -20,7 +20,6 @@ on:
|
||||
|
||||
jobs:
|
||||
smoke-test:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
|
||||
@@ -15,7 +15,6 @@ on:
|
||||
|
||||
jobs:
|
||||
save_repo_name:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Save Repo name'
|
||||
@@ -24,15 +23,14 @@ jobs:
|
||||
HEAD_SHA: '${{ github.event.inputs.head_sha || github.event.pull_request.head.sha }}'
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo "${REPO_NAME}" > ./pr/repo_name
|
||||
echo "${HEAD_SHA}" > ./pr/head_sha
|
||||
echo '${REPO_NAME}' > ./pr/repo_name
|
||||
echo '${HEAD_SHA}' > ./pr/head_sha
|
||||
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'repo_name'
|
||||
path: 'pr/'
|
||||
trigger_e2e:
|
||||
name: 'Trigger e2e'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- id: 'trigger-e2e'
|
||||
|
||||
@@ -28,7 +28,6 @@ on:
|
||||
|
||||
jobs:
|
||||
verify-release:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -80,37 +80,18 @@ manually during a session.
|
||||
|
||||
### Planning Workflow
|
||||
|
||||
Plan Mode uses an adaptive planning workflow where the research depth, plan
|
||||
structure, and consultation level are proportional to the task's complexity:
|
||||
|
||||
1. **Explore & Analyze:** Analyze requirements and use read-only tools to map
|
||||
affected modules and identify dependencies.
|
||||
2. **Consult:** The depth of consultation is proportional to the task's
|
||||
complexity:
|
||||
- **Simple Tasks:** Proceed directly to drafting.
|
||||
- **Standard Tasks:** Present a summary of viable approaches via
|
||||
[`ask_user`] for selection.
|
||||
- **Complex Tasks:** Present detailed trade-offs for at least two viable
|
||||
approaches via [`ask_user`] and obtain approval before drafting.
|
||||
3. **Draft:** Write a detailed implementation plan to the
|
||||
[plans directory](#custom-plan-directory-and-policies). The plan's structure
|
||||
adapts to the task:
|
||||
- **Simple Tasks:** Focused on specific **Changes** and **Verification**
|
||||
steps.
|
||||
- **Standard Tasks:** Includes an **Objective**, **Key Files & Context**,
|
||||
**Implementation Steps**, and **Verification & Testing**.
|
||||
- **Complex Tasks:** Comprehensive plans including **Background &
|
||||
Motivation**, **Scope & Impact**, **Proposed Solution**, **Alternatives
|
||||
Considered**, a phased **Implementation Plan**, **Verification**, and
|
||||
**Migration & Rollback** strategies.
|
||||
the codebase and validate assumptions. For complex tasks, identify at least
|
||||
two viable implementation approaches.
|
||||
2. **Consult:** Present a summary of the identified approaches via [`ask_user`]
|
||||
to obtain a selection. For simple or canonical tasks, this step may be
|
||||
skipped.
|
||||
3. **Draft:** Once an approach is selected, write a detailed implementation
|
||||
plan to the plans directory.
|
||||
4. **Review & Approval:** Use the [`exit_plan_mode`] tool to present the plan
|
||||
and formally request approval.
|
||||
- **Approve:** Exit Plan Mode and start implementation.
|
||||
- **Iterate:** Provide feedback to refine the plan.
|
||||
- **Refine manually:** Press **Ctrl + X** to open the plan file in your
|
||||
[preferred external editor]. This allows you to manually refine the plan
|
||||
steps before approval. The CLI will automatically refresh and show the
|
||||
updated plan after you save and close the editor.
|
||||
|
||||
For more complex or specialized planning tasks, you can
|
||||
[customize the planning workflow with skills](#customizing-planning-with-skills).
|
||||
@@ -309,4 +290,3 @@ performance. You can disable this automatic switching in your settings:
|
||||
https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/policy/policies/plan.toml
|
||||
[auto model]: /docs/reference/configuration.md#model-settings
|
||||
[model routing]: /docs/cli/telemetry.md#model-routing
|
||||
[preferred external editor]: /docs/reference/configuration.md#general
|
||||
|
||||
@@ -140,7 +140,6 @@ they appear in the UI.
|
||||
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
|
||||
| Enable Gemma Model Router | `experimental.gemmaModelRouter.enabled` | Enable the Gemma Model Router. Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim. | `false` |
|
||||
|
||||
### Skills
|
||||
|
||||
|
||||
@@ -176,12 +176,11 @@ Sends telemetry directly to Google Cloud services. No collector needed.
|
||||
}
|
||||
```
|
||||
2. Run Gemini CLI and send prompts.
|
||||
3. View logs, metrics, and traces:
|
||||
3. View logs and metrics:
|
||||
- Open the Google Cloud Console in your browser after sending prompts:
|
||||
- Logs (Logs Explorer): https://console.cloud.google.com/logs/
|
||||
- Metrics (Metrics Explorer):
|
||||
https://console.cloud.google.com/monitoring/metrics-explorer
|
||||
- Traces (Trace Explorer): https://console.cloud.google.com/traces/list
|
||||
- Logs: https://console.cloud.google.com/logs/
|
||||
- Metrics: https://console.cloud.google.com/monitoring/metrics-explorer
|
||||
- Traces: https://console.cloud.google.com/traces/list
|
||||
|
||||
### Collector-based export (advanced)
|
||||
|
||||
@@ -209,12 +208,11 @@ forward data to Google Cloud.
|
||||
- Save collector logs to `~/.gemini/tmp/<projectHash>/otel/collector-gcp.log`
|
||||
- Stop collector on exit (e.g. `Ctrl+C`)
|
||||
3. Run Gemini CLI and send prompts.
|
||||
4. View logs, metrics, and traces:
|
||||
4. View logs and metrics:
|
||||
- Open the Google Cloud Console in your browser after sending prompts:
|
||||
- Logs (Logs Explorer): https://console.cloud.google.com/logs/
|
||||
- Metrics (Metrics Explorer):
|
||||
https://console.cloud.google.com/monitoring/metrics-explorer
|
||||
- Traces (Trace Explorer): https://console.cloud.google.com/traces/list
|
||||
- Logs: https://console.cloud.google.com/logs/
|
||||
- Metrics: https://console.cloud.google.com/monitoring/metrics-explorer
|
||||
- Traces: https://console.cloud.google.com/traces/list
|
||||
- Open `~/.gemini/tmp/<projectHash>/otel/collector-gcp.log` to view local
|
||||
collector logs.
|
||||
|
||||
@@ -272,10 +270,10 @@ For local development and debugging, you can capture telemetry data locally:
|
||||
3. View traces at http://localhost:16686 and logs/metrics in the collector log
|
||||
file.
|
||||
|
||||
## Logs, metrics, and traces
|
||||
## Logs and metrics
|
||||
|
||||
The following section describes the structure of logs, metrics, and traces
|
||||
generated for Gemini CLI.
|
||||
The following section describes the structure of logs and metrics generated for
|
||||
Gemini CLI.
|
||||
|
||||
The `session.id`, `installation.id`, `active_approval_mode`, and `user.email`
|
||||
(available only when authenticated with a Google account) are included as common
|
||||
@@ -826,32 +824,6 @@ Optional performance monitoring for startup, CPU/memory, and phase timing.
|
||||
- `current_value` (number)
|
||||
- `baseline_value` (number)
|
||||
|
||||
### Traces
|
||||
|
||||
Traces offer a granular, "under-the-hood" view of every agent and backend
|
||||
operation. By providing a high-fidelity execution map, they enable precise
|
||||
debugging of complex tool interactions and deep performance optimization. Each
|
||||
trace captures rich, consistent metadata via custom span attributes:
|
||||
|
||||
- `gen_ai.operation.name` (string): The high-level operation kind (e.g.
|
||||
"tool_call", "llm_call").
|
||||
- `gen_ai.agent.name` (string): The service agent identifier ("gemini-cli").
|
||||
- `gen_ai.agent.description` (string): The service agent description.
|
||||
- `gen_ai.input.messages` (string): Input messages or metadata specific to the
|
||||
operation.
|
||||
- `gen_ai.output.messages` (string): Output messages or metadata generated from
|
||||
the operation.
|
||||
- `gen_ai.request.model` (string): The request model name.
|
||||
- `gen_ai.response.model` (string): The response model name.
|
||||
- `gen_ai.system_instructions` (json string): The system instructions.
|
||||
- `gen_ai.prompt.name` (string): The prompt name.
|
||||
- `gen_ai.tool.name` (string): The executed tool's name.
|
||||
- `gen_ai.tool.call_id` (string): The generated specific ID of the tool call.
|
||||
- `gen_ai.tool.description` (string): The executed tool's description.
|
||||
- `gen_ai.tool.definitions` (json string): The executed tool's description.
|
||||
- `gen_ai.conversation.id` (string): The current CLI session ID.
|
||||
- Additional user-defined Custom Attributes passed via the span's configuration.
|
||||
|
||||
#### GenAI semantic convention
|
||||
|
||||
The following metrics comply with [OpenTelemetry GenAI semantic conventions] for
|
||||
|
||||
@@ -227,42 +227,6 @@ skill definitions in a `skills/` directory. For example,
|
||||
Provide [sub-agents](../core/subagents.md) that users can delegate tasks to. Add
|
||||
agent definition files (`.md`) to an `agents/` directory in your extension root.
|
||||
|
||||
### <a id="policy-engine"></a>Policy Engine
|
||||
|
||||
Extensions can contribute policy rules and safety checkers to the Gemini CLI
|
||||
[Policy Engine](../reference/policy-engine.md). These rules are defined in
|
||||
`.toml` files and take effect when the extension is activated.
|
||||
|
||||
To add policies, create a `policies/` directory in your extension's root and
|
||||
place your `.toml` policy files inside it. Gemini CLI automatically loads all
|
||||
`.toml` files from this directory.
|
||||
|
||||
Rules contributed by extensions run in their own tier (tier 2), alongside
|
||||
workspace-defined policies. This tier has higher priority than the default rules
|
||||
but lower priority than user or admin policies.
|
||||
|
||||
> **Warning:** For security, Gemini CLI ignores any `allow` decisions or `yolo`
|
||||
> mode configurations in extension policies. This ensures that an extension
|
||||
> cannot automatically approve tool calls or bypass security measures without
|
||||
> your confirmation.
|
||||
|
||||
**Example `policies.toml`**
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
toolName = "my_server__dangerous_tool"
|
||||
decision = "ask_user"
|
||||
priority = 100
|
||||
|
||||
[[safety_checker]]
|
||||
toolName = "my_server__write_data"
|
||||
priority = 200
|
||||
[safety_checker.checker]
|
||||
type = "in-process"
|
||||
name = "allowed-path"
|
||||
required_context = ["environment"]
|
||||
```
|
||||
|
||||
### Themes
|
||||
|
||||
Extensions can provide custom themes to personalize the CLI UI. Themes are
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
# Local development guide
|
||||
|
||||
This guide provides instructions for setting up and using local development
|
||||
features, such as tracing.
|
||||
features, such as development tracing.
|
||||
|
||||
## Tracing
|
||||
## Development tracing
|
||||
|
||||
Traces are OpenTelemetry (OTel) records that help you debug your code by
|
||||
instrumenting key events like model calls, tool scheduler operations, and tool
|
||||
calls.
|
||||
Development traces (dev traces) are OpenTelemetry (OTel) traces that help you
|
||||
debug your code by instrumenting interesting events like model calls, tool
|
||||
scheduler, tool calls, etc.
|
||||
|
||||
Traces provide deep visibility into agent behavior and are invaluable for
|
||||
debugging complex issues. They are captured automatically when telemetry is
|
||||
enabled.
|
||||
Dev traces are verbose and are specifically meant for understanding agent
|
||||
behavior and debugging issues. They are disabled by default.
|
||||
|
||||
### Viewing traces
|
||||
To enable dev traces, set the `GEMINI_DEV_TRACING=true` environment variable
|
||||
when running Gemini CLI.
|
||||
|
||||
You can view traces using either Jaeger or the Genkit Developer UI.
|
||||
### Viewing dev traces
|
||||
|
||||
You can view dev traces using either Jaeger or the Genkit Developer UI.
|
||||
|
||||
#### Using Genkit
|
||||
|
||||
@@ -35,12 +37,13 @@ Genkit provides a web-based UI for viewing traces and other telemetry data.
|
||||
Genkit Developer UI: http://localhost:4000
|
||||
```
|
||||
|
||||
2. **Run Gemini CLI:**
|
||||
2. **Run Gemini CLI with dev tracing:**
|
||||
|
||||
In a separate terminal, run your Gemini CLI command:
|
||||
In a separate terminal, run your Gemini CLI command with the
|
||||
`GEMINI_DEV_TRACING` environment variable:
|
||||
|
||||
```bash
|
||||
gemini
|
||||
GEMINI_DEV_TRACING=true gemini
|
||||
```
|
||||
|
||||
3. **View the traces:**
|
||||
@@ -50,7 +53,7 @@ Genkit provides a web-based UI for viewing traces and other telemetry data.
|
||||
|
||||
#### Using Jaeger
|
||||
|
||||
You can view traces in the Jaeger UI. To get started, follow these steps:
|
||||
You can view dev traces in the Jaeger UI. To get started, follow these steps:
|
||||
|
||||
1. **Start the telemetry collector:**
|
||||
|
||||
@@ -64,12 +67,13 @@ You can view traces in the Jaeger UI. To get started, follow these steps:
|
||||
This command also configures your workspace for local telemetry and provides
|
||||
a link to the Jaeger UI (usually `http://localhost:16686`).
|
||||
|
||||
2. **Run Gemini CLI:**
|
||||
2. **Run Gemini CLI with dev tracing:**
|
||||
|
||||
In a separate terminal, run your Gemini CLI command:
|
||||
In a separate terminal, run your Gemini CLI command with the
|
||||
`GEMINI_DEV_TRACING` environment variable:
|
||||
|
||||
```bash
|
||||
gemini
|
||||
GEMINI_DEV_TRACING=true gemini
|
||||
```
|
||||
|
||||
3. **View the traces:**
|
||||
@@ -80,10 +84,10 @@ You can view traces in the Jaeger UI. To get started, follow these steps:
|
||||
For more detailed information on telemetry, see the
|
||||
[telemetry documentation](./cli/telemetry.md).
|
||||
|
||||
### Instrumenting code with traces
|
||||
### Instrumenting code with dev traces
|
||||
|
||||
You can add traces to your own code for more detailed instrumentation. This is
|
||||
useful for debugging and understanding the flow of execution.
|
||||
You can add dev traces to your own code for more detailed instrumentation. This
|
||||
is useful for debugging and understanding the flow of execution.
|
||||
|
||||
Use the `runInDevTraceSpan` function to wrap any section of code in a trace
|
||||
span.
|
||||
@@ -92,39 +96,29 @@ Here is a basic example:
|
||||
|
||||
```typescript
|
||||
import { runInDevTraceSpan } from '@google/gemini-cli-core';
|
||||
import { GeminiCliOperation } from '@google/gemini-cli-core/lib/telemetry/constants.js';
|
||||
|
||||
await runInDevTraceSpan(
|
||||
{
|
||||
operation: GeminiCliOperation.ToolCall,
|
||||
attributes: {
|
||||
[GEN_AI_AGENT_NAME]: 'gemini-cli',
|
||||
},
|
||||
},
|
||||
async ({ metadata }) => {
|
||||
// The `metadata` object allows you to record the input and output of the
|
||||
// operation as well as other attributes.
|
||||
metadata.input = { key: 'value' };
|
||||
// Set custom attributes.
|
||||
metadata.attributes['custom.attribute'] = 'custom.value';
|
||||
await runInDevTraceSpan({ name: 'my-custom-span' }, async ({ metadata }) => {
|
||||
// The `metadata` object allows you to record the input and output of the
|
||||
// operation as well as other attributes.
|
||||
metadata.input = { key: 'value' };
|
||||
// Set custom attributes.
|
||||
metadata.attributes['gen_ai.request.model'] = 'gemini-4.0-mega';
|
||||
|
||||
// Your code to be traced goes here
|
||||
try {
|
||||
const output = await somethingRisky();
|
||||
metadata.output = output;
|
||||
return output;
|
||||
} catch (e) {
|
||||
metadata.error = e;
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
);
|
||||
// Your code to be traced goes here
|
||||
try {
|
||||
const output = await somethingRisky();
|
||||
metadata.output = output;
|
||||
return output;
|
||||
} catch (e) {
|
||||
metadata.error = e;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
- `operation`: The operation type of the span, represented by the
|
||||
`GeminiCliOperation` enum.
|
||||
- `name`: The name of the span, which will be displayed in the trace.
|
||||
- `metadata.input`: (Optional) An object containing the input data for the
|
||||
traced operation.
|
||||
- `metadata.output`: (Optional) An object containing the output data from the
|
||||
|
||||
@@ -1014,23 +1014,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.enabled`** (boolean):
|
||||
- **Description:** Enable the Gemma Model Router. Requires a local endpoint
|
||||
serving Gemma via the Gemini API using LiteRT-LM shim.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.classifier.host`** (string):
|
||||
- **Description:** The host of the classifier.
|
||||
- **Default:** `"http://localhost:9379"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.classifier.model`** (string):
|
||||
- **Description:** The model to use for the classifier. Only tested on
|
||||
`gemma3-1b-gpu-custom`.
|
||||
- **Default:** `"gemma3-1b-gpu-custom"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `skills`
|
||||
|
||||
- **`skills.enabled`** (boolean):
|
||||
|
||||
@@ -87,12 +87,12 @@ available combinations.
|
||||
|
||||
#### Text Input
|
||||
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| Submit the current prompt. | `Enter (no Shift, Alt, Ctrl, Cmd)` |
|
||||
| Insert a newline without submitting. | `Ctrl + Enter`<br />`Cmd + Enter`<br />`Alt + Enter`<br />`Shift + Enter`<br />`Ctrl + J` |
|
||||
| Open the current prompt or the plan in an external editor. | `Ctrl + X` |
|
||||
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V`<br />`Alt + V` |
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| Submit the current prompt. | `Enter (no Shift, Alt, Ctrl, Cmd)` |
|
||||
| Insert a newline without submitting. | `Ctrl + Enter`<br />`Cmd + Enter`<br />`Alt + Enter`<br />`Shift + Enter`<br />`Ctrl + J` |
|
||||
| Open the current prompt in an external editor. | `Ctrl + X` |
|
||||
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V`<br />`Alt + V` |
|
||||
|
||||
#### App Controls
|
||||
|
||||
|
||||
@@ -97,10 +97,9 @@ has a designated number that forms the base of the final priority calculation.
|
||||
| Tier | Base | Description |
|
||||
| :-------- | :--- | :------------------------------------------------------------------------- |
|
||||
| Default | 1 | Built-in policies that ship with the Gemini CLI. |
|
||||
| Extension | 2 | Policies defined in extensions. |
|
||||
| Workspace | 3 | Policies defined in the current workspace's configuration directory. |
|
||||
| User | 4 | Custom policies defined by the user. |
|
||||
| Admin | 5 | Policies managed by an administrator (e.g., in an enterprise environment). |
|
||||
| Workspace | 2 | Policies defined in the current workspace's configuration directory. |
|
||||
| User | 3 | Custom policies defined by the user. |
|
||||
| Admin | 4 | Policies managed by an administrator (e.g., in an enterprise environment). |
|
||||
|
||||
Within a TOML policy file, you assign a priority value from **0 to 999**. The
|
||||
engine transforms this into a final priority using the following formula:
|
||||
|
||||
@@ -5,15 +5,6 @@ problems encountered while using Gemini CLI.
|
||||
|
||||
## General issues
|
||||
|
||||
### Why can't I use third-party software (e.g. Claude Code, OpenClaw, OpenCode) with Gemini CLI?
|
||||
|
||||
Using third-party software, tools, or services to access Gemini CLI is a
|
||||
violation of our [applicable terms and policies](tos-privacy.md), and severely
|
||||
degrades the experience for legitimate product users. Such actions may be
|
||||
grounds for suspension or termination of your account. If you would like to use
|
||||
a third-party coding agent with Gemini, we recommend using a Vertex or AI Studio
|
||||
API key.
|
||||
|
||||
### Why am I getting an `API error: 429 - Resource exhausted`?
|
||||
|
||||
This error indicates that you have exceeded your API request limit. The Gemini
|
||||
|
||||
@@ -7,12 +7,6 @@ is licensed under the
|
||||
When you use Gemini CLI to access or use Google’s services, the Terms of Service
|
||||
and Privacy Notices applicable to those services apply to such access and use.
|
||||
|
||||
Directly accessing the services powering Gemini CLI (e.g., the Gemini Code
|
||||
Assist service) using third-party software, tools, or services (for example,
|
||||
using OpenClaw with Gemini CLI OAuth) is a violation of applicable terms and
|
||||
policies. Such actions may be grounds for suspension or termination of your
|
||||
account.
|
||||
|
||||
Your Gemini CLI Usage Statistics are handled in accordance with Google's Privacy
|
||||
Policy.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
describe('validation_fidelity', () => {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
name: 'should perform exhaustive validation autonomously when guided by system instructions',
|
||||
files: {
|
||||
'src/types.ts': `
|
||||
|
||||
@@ -72,6 +72,7 @@ describe('ACP telemetry', () => {
|
||||
GEMINI_TELEMETRY_ENABLED: 'true',
|
||||
GEMINI_TELEMETRY_TARGET: 'local',
|
||||
GEMINI_TELEMETRY_OUTFILE: telemetryPath,
|
||||
// GEMINI_DEV_TRACING not set: fake responses aren't instrumented for spans
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2292,6 +2292,7 @@
|
||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.2",
|
||||
@@ -2472,6 +2473,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -2521,6 +2523,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
|
||||
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
@@ -2895,6 +2898,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
|
||||
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -2928,6 +2932,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz",
|
||||
"integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/resources": "2.5.0"
|
||||
@@ -2982,6 +2987,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz",
|
||||
"integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/resources": "2.5.0",
|
||||
@@ -4178,6 +4184,7 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4451,6 +4458,7 @@
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
@@ -5298,6 +5306,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -7851,6 +7860,7 @@
|
||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8483,6 +8493,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -9777,6 +9788,7 @@
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz",
|
||||
"integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -10056,6 +10068,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz",
|
||||
"integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -13705,6 +13718,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13715,6 +13729,7 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -15674,6 +15689,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15897,7 +15913,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -15905,6 +15922,7 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -16064,6 +16082,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16272,6 +16291,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -16385,6 +16405,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16397,6 +16418,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -17041,6 +17063,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -17440,6 +17463,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -15,11 +15,4 @@
|
||||
- **Utilities**: Use `renderWithProviders` and `waitFor` from
|
||||
`packages/cli/src/test-utils/`.
|
||||
- **Snapshots**: Use `toMatchSnapshot()` to verify Ink output.
|
||||
- **SVG Snapshots**: Use `await expect(renderResult).toMatchSvgSnapshot()` for
|
||||
UI components whenever colors or detailed visual layout matter. SVG snapshots
|
||||
capture styling accurately. Make sure to await the `waitUntilReady()` of the
|
||||
render result before asserting. After updating SVG snapshots, always examine
|
||||
the resulting `.svg` files (e.g. by reading their content or visually
|
||||
inspecting them) to ensure the render and colors actually look as expected and
|
||||
don't just contain an error message.
|
||||
- **Mocks**: Use mocks as sparingly as possible.
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# Policy engine example extension
|
||||
|
||||
This extension demonstrates how to contribute security rules and safety checkers
|
||||
to the Gemini CLI Policy Engine.
|
||||
|
||||
## Description
|
||||
|
||||
The extension uses a `policies/` directory containing `.toml` files to define:
|
||||
|
||||
- A rule that requires user confirmation for `rm -rf` commands.
|
||||
- A rule that denies searching for sensitive files (like `.env`) using `grep`.
|
||||
- A safety checker that validates file paths for all write operations.
|
||||
|
||||
## Structure
|
||||
|
||||
- `gemini-extension.json`: The manifest file.
|
||||
- `policies/`: Contains the `.toml` policy files.
|
||||
|
||||
## How to use
|
||||
|
||||
1. Link this extension to your local Gemini CLI installation:
|
||||
|
||||
```bash
|
||||
gemini extensions link packages/cli/src/commands/extensions/examples/policies
|
||||
```
|
||||
|
||||
2. Restart your Gemini CLI session.
|
||||
|
||||
3. **Observe the policies:**
|
||||
- Try asking the model to delete a directory: The policy engine will prompt
|
||||
you for confirmation due to the `rm -rf` rule.
|
||||
- Try asking the model to search for secrets: The `grep` rule will deny the
|
||||
request and display the custom deny message.
|
||||
- Any file write operation will now be processed through the `allowed-path`
|
||||
safety checker.
|
||||
|
||||
## Security note
|
||||
|
||||
For security, Gemini CLI ignores any `allow` decisions or `yolo` mode
|
||||
configurations contributed by extensions. This ensures that extensions can
|
||||
strengthen security but cannot bypass user confirmation.
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "policy-example",
|
||||
"version": "1.0.0",
|
||||
"description": "An example extension demonstrating Policy Engine support."
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
# Example Policy Rules for Gemini CLI Extension
|
||||
#
|
||||
# Extensions run in Tier 2 (Extension Tier).
|
||||
# Security Note: 'allow' decisions and 'yolo' mode configurations are ignored.
|
||||
|
||||
# Rule: Always ask the user before running a specific dangerous shell command.
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "rm -rf"
|
||||
decision = "ask_user"
|
||||
priority = 100
|
||||
|
||||
# Rule: Deny access to sensitive files using the grep tool.
|
||||
[[rule]]
|
||||
toolName = "grep_search"
|
||||
argsPattern = "(\.env|id_rsa|passwd)"
|
||||
decision = "deny"
|
||||
priority = 200
|
||||
deny_message = "Access to sensitive credentials or system files is restricted by the policy-example extension."
|
||||
|
||||
# Safety Checker: Apply path validation to all write operations.
|
||||
[[safety_checker]]
|
||||
toolName = ["write_file", "replace"]
|
||||
priority = 300
|
||||
[safety_checker.checker]
|
||||
type = "in-process"
|
||||
name = "allowed-path"
|
||||
required_context = ["environment"]
|
||||
@@ -2765,66 +2765,6 @@ describe('loadCliConfig approval mode', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig gemmaModelRouter', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should have gemmaModelRouter disabled by default', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings();
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getGemmaModelRouterEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should load gemmaModelRouter settings from merged settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: {
|
||||
gemmaModelRouter: {
|
||||
enabled: true,
|
||||
classifier: {
|
||||
host: 'http://custom:1234',
|
||||
model: 'custom-gemma',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getGemmaModelRouterEnabled()).toBe(true);
|
||||
const gemmaSettings = config.getGemmaModelRouterSettings();
|
||||
expect(gemmaSettings.classifier?.host).toBe('http://custom:1234');
|
||||
expect(gemmaSettings.classifier?.model).toBe('custom-gemma');
|
||||
});
|
||||
|
||||
it('should handle partial gemmaModelRouter settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: {
|
||||
gemmaModelRouter: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getGemmaModelRouterEnabled()).toBe(true);
|
||||
const gemmaSettings = config.getGemmaModelRouterSettings();
|
||||
expect(gemmaSettings.classifier?.host).toBe('http://localhost:9379');
|
||||
expect(gemmaSettings.classifier?.model).toBe('gemma3-1b-gpu-custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig fileFiltering', () => {
|
||||
const originalArgv = process.argv;
|
||||
|
||||
|
||||
@@ -843,7 +843,6 @@ export async function loadCliConfig(
|
||||
interactive,
|
||||
trustedFolder,
|
||||
useBackgroundColor: settings.ui?.useBackgroundColor,
|
||||
useAlternateBuffer: settings.ui?.useAlternateBuffer,
|
||||
useRipgrep: settings.tools?.useRipgrep,
|
||||
enableInteractiveShell: settings.tools?.shell?.enableInteractiveShell,
|
||||
shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout,
|
||||
@@ -857,7 +856,6 @@ export async function loadCliConfig(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
|
||||
},
|
||||
gemmaModelRouter: settings.experimental?.gemmaModelRouter,
|
||||
fakeResponses: argv.fakeResponses,
|
||||
recordResponses: argv.recordResponses,
|
||||
retryFetchErrors: settings.general?.retryFetchErrors,
|
||||
|
||||
@@ -52,10 +52,6 @@ import {
|
||||
applyAdminAllowlist,
|
||||
getAdminBlockedMcpServersMessage,
|
||||
CoreToolCallStatus,
|
||||
loadExtensionPolicies,
|
||||
isSubpath,
|
||||
type PolicyRule,
|
||||
type SafetyCheckerRule,
|
||||
HookType,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { maybeRequestConsentOrFail } from './extensions/consent.js';
|
||||
@@ -768,18 +764,9 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
|
||||
const contextFiles = getContextFileNames(config)
|
||||
.map((contextFileName) => {
|
||||
const contextFilePath = path.join(
|
||||
effectiveExtensionPath,
|
||||
contextFileName,
|
||||
);
|
||||
if (!isSubpath(effectiveExtensionPath, contextFilePath)) {
|
||||
throw new Error(
|
||||
`Invalid context file path: "${contextFileName}". Context files must be within the extension directory.`,
|
||||
);
|
||||
}
|
||||
return contextFilePath;
|
||||
})
|
||||
.map((contextFileName) =>
|
||||
path.join(effectiveExtensionPath, contextFileName),
|
||||
)
|
||||
.filter((contextFilePath) => fs.existsSync(contextFilePath));
|
||||
|
||||
const hydrationContext: VariableContext = {
|
||||
@@ -833,24 +820,6 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
recursivelyHydrateStrings(skill, hydrationContext),
|
||||
);
|
||||
|
||||
let rules: PolicyRule[] | undefined;
|
||||
let checkers: SafetyCheckerRule[] | undefined;
|
||||
|
||||
const policyDir = path.join(effectiveExtensionPath, 'policies');
|
||||
if (fs.existsSync(policyDir)) {
|
||||
const result = await loadExtensionPolicies(config.name, policyDir);
|
||||
rules = result.rules;
|
||||
checkers = result.checkers;
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
for (const error of result.errors) {
|
||||
debugLogger.warn(
|
||||
`[ExtensionManager] Error loading policies from ${config.name}: ${error.message}${error.details ? `\nDetails: ${error.details}` : ''}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const agentLoadResult = await loadAgentsFromDirectory(
|
||||
path.join(effectiveExtensionPath, 'agents'),
|
||||
);
|
||||
@@ -884,8 +853,6 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
skills,
|
||||
agents: agentLoadResult.agents,
|
||||
themes: config.themes,
|
||||
rules,
|
||||
checkers,
|
||||
};
|
||||
} catch (e) {
|
||||
debugLogger.error(
|
||||
|
||||
@@ -239,27 +239,6 @@ describe('extension tests', () => {
|
||||
expect(extensions[0].name).toBe('test-extension');
|
||||
});
|
||||
|
||||
it('should throw an error if a context file path is outside the extension directory', async () => {
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'traversal-extension',
|
||||
version: '1.0.0',
|
||||
contextFileName: '../secret.txt',
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
expect(extensions).toHaveLength(0);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'traversal-extension: Invalid context file path: "../secret.txt"',
|
||||
),
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should load context file path when GEMINI.md is present', async () => {
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
@@ -384,111 +363,6 @@ describe('extension tests', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should load extension policies from the policies directory', async () => {
|
||||
const extDir = createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'policy-extension',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const policiesDir = path.join(extDir, 'policies');
|
||||
fs.mkdirSync(policiesDir);
|
||||
|
||||
const policiesContent = `
|
||||
[[rule]]
|
||||
toolName = "deny_tool"
|
||||
decision = "deny"
|
||||
priority = 500
|
||||
|
||||
[[rule]]
|
||||
toolName = "ask_tool"
|
||||
decision = "ask_user"
|
||||
priority = 100
|
||||
`;
|
||||
fs.writeFileSync(
|
||||
path.join(policiesDir, 'policies.toml'),
|
||||
policiesContent,
|
||||
);
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
expect(extensions).toHaveLength(1);
|
||||
const extension = extensions[0];
|
||||
|
||||
expect(extension.rules).toBeDefined();
|
||||
expect(extension.rules).toHaveLength(2);
|
||||
expect(
|
||||
extension.rules!.find((r) => r.toolName === 'deny_tool')?.decision,
|
||||
).toBe('deny');
|
||||
expect(
|
||||
extension.rules!.find((r) => r.toolName === 'ask_tool')?.decision,
|
||||
).toBe('ask_user');
|
||||
// Verify source is prefixed
|
||||
expect(extension.rules![0].source).toContain(
|
||||
'Extension (policy-extension):',
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore ALLOW rules and YOLO mode from extension policies for security', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const extDir = createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'security-test-extension',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const policiesDir = path.join(extDir, 'policies');
|
||||
fs.mkdirSync(policiesDir);
|
||||
|
||||
const policiesContent = `
|
||||
[[rule]]
|
||||
toolName = "allow_tool"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "yolo_tool"
|
||||
decision = "ask_user"
|
||||
priority = 100
|
||||
modes = ["yolo"]
|
||||
|
||||
[[safety_checker]]
|
||||
toolName = "yolo_check"
|
||||
priority = 100
|
||||
modes = ["yolo"]
|
||||
[safety_checker.checker]
|
||||
type = "external"
|
||||
name = "yolo-checker"
|
||||
`;
|
||||
fs.writeFileSync(
|
||||
path.join(policiesDir, 'policies.toml'),
|
||||
policiesContent,
|
||||
);
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
expect(extensions).toHaveLength(1);
|
||||
const extension = extensions[0];
|
||||
|
||||
// ALLOW rules and YOLO rules/checkers should be filtered out
|
||||
expect(extension.rules).toBeDefined();
|
||||
expect(extension.rules).toHaveLength(0);
|
||||
expect(extension.checkers).toBeDefined();
|
||||
expect(extension.checkers).toHaveLength(0);
|
||||
|
||||
// Should have logged warnings
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('attempted to contribute an ALLOW rule'),
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('attempted to contribute a rule for YOLO mode'),
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'attempted to contribute a safety checker for YOLO mode',
|
||||
),
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should hydrate ${extensionPath} correctly for linked extensions', async () => {
|
||||
const sourceExtDir = getRealPath(
|
||||
createExtension({
|
||||
@@ -666,7 +540,7 @@ name = "yolo-checker"
|
||||
|
||||
// Bad extension
|
||||
const badExtDir = path.join(userExtensionsDir, 'bad-ext');
|
||||
fs.mkdirSync(badExtDir, { recursive: true });
|
||||
fs.mkdirSync(badExtDir);
|
||||
const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
|
||||
fs.writeFileSync(badConfigPath, '{ "name": "bad-ext"'); // Malformed
|
||||
|
||||
@@ -674,7 +548,7 @@ name = "yolo-checker"
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].name).toBe('good-ext');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.stringContaining(
|
||||
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`,
|
||||
),
|
||||
@@ -697,7 +571,7 @@ name = "yolo-checker"
|
||||
|
||||
// Bad extension
|
||||
const badExtDir = path.join(userExtensionsDir, 'bad-ext-no-name');
|
||||
fs.mkdirSync(badExtDir, { recursive: true });
|
||||
fs.mkdirSync(badExtDir);
|
||||
const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
|
||||
fs.writeFileSync(badConfigPath, JSON.stringify({ version: '1.0.0' }));
|
||||
|
||||
@@ -705,7 +579,7 @@ name = "yolo-checker"
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].name).toBe('good-ext');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.stringContaining(
|
||||
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
|
||||
),
|
||||
|
||||
@@ -489,7 +489,7 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||
[Command.SUBMIT]: 'Submit the current prompt.',
|
||||
[Command.NEWLINE]: 'Insert a newline without submitting.',
|
||||
[Command.OPEN_EXTERNAL_EDITOR]:
|
||||
'Open the current prompt or the plan in an external editor.',
|
||||
'Open the current prompt in an external editor.',
|
||||
[Command.PASTE_CLIPBOARD]: 'Paste from the clipboard.',
|
||||
|
||||
// App Controls
|
||||
|
||||
@@ -177,13 +177,13 @@ describe('Policy Engine Integration Tests', () => {
|
||||
);
|
||||
const engine = new PolicyEngine(config);
|
||||
|
||||
// MCP server allowed (priority 4.1) provides general allow for server
|
||||
// MCP server allowed (priority 4.1) provides general allow for server
|
||||
// MCP server allowed (priority 3.1) provides general allow for server
|
||||
// MCP server allowed (priority 3.1) provides general allow for server
|
||||
expect(
|
||||
(await engine.check({ name: 'my-server__safe-tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
// But specific tool exclude (priority 4.4) wins over server allow
|
||||
// But specific tool exclude (priority 3.4) wins over server allow
|
||||
expect(
|
||||
(await engine.check({ name: 'my-server__dangerous-tool' }, undefined))
|
||||
.decision,
|
||||
@@ -476,25 +476,25 @@ describe('Policy Engine Integration Tests', () => {
|
||||
|
||||
// Find rules and verify their priorities
|
||||
const blockedToolRule = rules.find((r) => r.toolName === 'blocked-tool');
|
||||
expect(blockedToolRule?.priority).toBe(4.4); // Command line exclude
|
||||
expect(blockedToolRule?.priority).toBe(3.4); // Command line exclude
|
||||
|
||||
const blockedServerRule = rules.find(
|
||||
(r) => r.toolName === 'blocked-server__*',
|
||||
);
|
||||
expect(blockedServerRule?.priority).toBe(4.9); // MCP server exclude
|
||||
expect(blockedServerRule?.priority).toBe(3.9); // MCP server exclude
|
||||
|
||||
const specificToolRule = rules.find(
|
||||
(r) => r.toolName === 'specific-tool',
|
||||
);
|
||||
expect(specificToolRule?.priority).toBe(4.3); // Command line allow
|
||||
expect(specificToolRule?.priority).toBe(3.3); // Command line allow
|
||||
|
||||
const trustedServerRule = rules.find(
|
||||
(r) => r.toolName === 'trusted-server__*',
|
||||
);
|
||||
expect(trustedServerRule?.priority).toBe(4.2); // MCP trusted server
|
||||
expect(trustedServerRule?.priority).toBe(3.2); // MCP trusted server
|
||||
|
||||
const mcpServerRule = rules.find((r) => r.toolName === 'mcp-server__*');
|
||||
expect(mcpServerRule?.priority).toBe(4.1); // MCP allowed server
|
||||
expect(mcpServerRule?.priority).toBe(3.1); // MCP allowed server
|
||||
|
||||
const readOnlyToolRule = rules.find((r) => r.toolName === 'glob');
|
||||
// Priority 70 in default tier → 1.07 (Overriding Plan Mode Deny)
|
||||
@@ -641,16 +641,16 @@ describe('Policy Engine Integration Tests', () => {
|
||||
|
||||
// Verify each rule has the expected priority
|
||||
const tool3Rule = rules.find((r) => r.toolName === 'tool3');
|
||||
expect(tool3Rule?.priority).toBe(4.4); // Excluded tools (user tier)
|
||||
expect(tool3Rule?.priority).toBe(3.4); // Excluded tools (user tier)
|
||||
|
||||
const server2Rule = rules.find((r) => r.toolName === 'server2__*');
|
||||
expect(server2Rule?.priority).toBe(4.9); // Excluded servers (user tier)
|
||||
expect(server2Rule?.priority).toBe(3.9); // Excluded servers (user tier)
|
||||
|
||||
const tool1Rule = rules.find((r) => r.toolName === 'tool1');
|
||||
expect(tool1Rule?.priority).toBe(4.3); // Allowed tools (user tier)
|
||||
expect(tool1Rule?.priority).toBe(3.3); // Allowed tools (user tier)
|
||||
|
||||
const server1Rule = rules.find((r) => r.toolName === 'server1__*');
|
||||
expect(server1Rule?.priority).toBe(4.1); // Allowed servers (user tier)
|
||||
expect(server1Rule?.priority).toBe(3.1); // Allowed servers (user tier)
|
||||
|
||||
const globRule = rules.find((r) => r.toolName === 'glob');
|
||||
// Priority 70 in default tier → 1.07
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
resolveWorkspacePolicyState,
|
||||
autoAcceptWorkspacePolicies,
|
||||
setAutoAcceptWorkspacePolicies,
|
||||
disableWorkspacePolicies,
|
||||
setDisableWorkspacePolicies,
|
||||
} from './policy.js';
|
||||
import { writeToStderr } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -47,9 +45,6 @@ describe('resolveWorkspacePolicyState', () => {
|
||||
fs.mkdirSync(workspaceDir);
|
||||
policiesDir = path.join(workspaceDir, '.gemini', 'policies');
|
||||
|
||||
// Enable policies for these tests to verify loading logic
|
||||
setDisableWorkspacePolicies(false);
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -72,13 +67,6 @@ describe('resolveWorkspacePolicyState', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have disableWorkspacePolicies set to true by default', () => {
|
||||
// We explicitly set it to false in beforeEach for other tests,
|
||||
// so here we test that setting it to true works.
|
||||
setDisableWorkspacePolicies(true);
|
||||
expect(disableWorkspacePolicies).toBe(true);
|
||||
});
|
||||
|
||||
it('should return policy directory if integrity matches', async () => {
|
||||
// Set up policies directory with a file
|
||||
fs.mkdirSync(policiesDir, { recursive: true });
|
||||
@@ -200,26 +188,7 @@ describe('resolveWorkspacePolicyState', () => {
|
||||
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return empty state if disableWorkspacePolicies is true even if folder is trusted', async () => {
|
||||
setDisableWorkspacePolicies(true);
|
||||
|
||||
// Set up policies directory with a file
|
||||
fs.mkdirSync(policiesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
||||
|
||||
const result = await resolveWorkspacePolicyState({
|
||||
cwd: workspaceDir,
|
||||
trustedFolder: true,
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
workspacePoliciesDir: undefined,
|
||||
policyUpdateConfirmationRequest: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty state if cwd is a symlink to the home directory', async () => {
|
||||
it('should not return workspace policies if cwd is a symlink to the home directory', async () => {
|
||||
const policiesDir = path.join(tempDir, '.gemini', 'policies');
|
||||
fs.mkdirSync(policiesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
||||
|
||||
@@ -35,20 +35,6 @@ export function setAutoAcceptWorkspacePolicies(value: boolean) {
|
||||
autoAcceptWorkspacePolicies = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary flag to disable workspace level policies altogether.
|
||||
* Exported as 'let' to allow monkey patching in tests via the setter.
|
||||
*/
|
||||
export let disableWorkspacePolicies = true;
|
||||
|
||||
/**
|
||||
* Sets the disableWorkspacePolicies flag.
|
||||
* Used primarily for testing purposes.
|
||||
*/
|
||||
export function setDisableWorkspacePolicies(value: boolean) {
|
||||
disableWorkspacePolicies = value;
|
||||
}
|
||||
|
||||
export async function createPolicyEngineConfig(
|
||||
settings: Settings,
|
||||
approvalMode: ApprovalMode,
|
||||
@@ -95,7 +81,7 @@ export async function resolveWorkspacePolicyState(options: {
|
||||
| PolicyUpdateConfirmationRequest
|
||||
| undefined;
|
||||
|
||||
if (trustedFolder && !disableWorkspacePolicies) {
|
||||
if (trustedFolder) {
|
||||
const storage = new Storage(cwd);
|
||||
|
||||
// If we are in the home directory (or rather, our target Gemini dir is the global one),
|
||||
|
||||
@@ -444,60 +444,6 @@ describe('SettingsSchema', () => {
|
||||
expect(hookItemProperties.description).toBeDefined();
|
||||
expect(hookItemProperties.description.type).toBe('string');
|
||||
});
|
||||
|
||||
it('should have gemmaModelRouter setting in schema', () => {
|
||||
const gemmaModelRouter =
|
||||
getSettingsSchema().experimental.properties.gemmaModelRouter;
|
||||
expect(gemmaModelRouter).toBeDefined();
|
||||
expect(gemmaModelRouter.type).toBe('object');
|
||||
expect(gemmaModelRouter.category).toBe('Experimental');
|
||||
expect(gemmaModelRouter.default).toEqual({});
|
||||
expect(gemmaModelRouter.requiresRestart).toBe(true);
|
||||
expect(gemmaModelRouter.showInDialog).toBe(true);
|
||||
expect(gemmaModelRouter.description).toBe(
|
||||
'Enable Gemma model router (experimental).',
|
||||
);
|
||||
|
||||
const enabled = gemmaModelRouter.properties.enabled;
|
||||
expect(enabled).toBeDefined();
|
||||
expect(enabled.type).toBe('boolean');
|
||||
expect(enabled.category).toBe('Experimental');
|
||||
expect(enabled.default).toBe(false);
|
||||
expect(enabled.requiresRestart).toBe(true);
|
||||
expect(enabled.showInDialog).toBe(true);
|
||||
expect(enabled.description).toBe(
|
||||
'Enable the Gemma Model Router. Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim.',
|
||||
);
|
||||
|
||||
const classifier = gemmaModelRouter.properties.classifier;
|
||||
expect(classifier).toBeDefined();
|
||||
expect(classifier.type).toBe('object');
|
||||
expect(classifier.category).toBe('Experimental');
|
||||
expect(classifier.default).toEqual({});
|
||||
expect(classifier.requiresRestart).toBe(true);
|
||||
expect(classifier.showInDialog).toBe(false);
|
||||
expect(classifier.description).toBe('Classifier configuration.');
|
||||
|
||||
const host = classifier.properties.host;
|
||||
expect(host).toBeDefined();
|
||||
expect(host.type).toBe('string');
|
||||
expect(host.category).toBe('Experimental');
|
||||
expect(host.default).toBe('http://localhost:9379');
|
||||
expect(host.requiresRestart).toBe(true);
|
||||
expect(host.showInDialog).toBe(false);
|
||||
expect(host.description).toBe('The host of the classifier.');
|
||||
|
||||
const model = classifier.properties.model;
|
||||
expect(model).toBeDefined();
|
||||
expect(model.type).toBe('string');
|
||||
expect(model.category).toBe('Experimental');
|
||||
expect(model.default).toBe('gemma3-1b-gpu-custom');
|
||||
expect(model.requiresRestart).toBe(true);
|
||||
expect(model.showInDialog).toBe(false);
|
||||
expect(model.description).toBe(
|
||||
'The model to use for the classifier. Only tested on `gemma3-1b-gpu-custom`.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('has JSON schema definitions for every referenced ref', () => {
|
||||
|
||||
@@ -1787,57 +1787,6 @@ const SETTINGS_SCHEMA = {
|
||||
'Enable web fetch behavior that bypasses LLM summarization.',
|
||||
showInDialog: true,
|
||||
},
|
||||
gemmaModelRouter: {
|
||||
type: 'object',
|
||||
label: 'Gemma Model Router',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Enable Gemma model router (experimental).',
|
||||
showInDialog: true,
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Gemma Model Router',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable the Gemma Model Router. Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim.',
|
||||
showInDialog: true,
|
||||
},
|
||||
classifier: {
|
||||
type: 'object',
|
||||
label: 'Classifier',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Classifier configuration.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
host: {
|
||||
type: 'string',
|
||||
label: 'Host',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: 'http://localhost:9379',
|
||||
description: 'The host of the classifier.',
|
||||
showInDialog: false,
|
||||
},
|
||||
model: {
|
||||
type: 'string',
|
||||
label: 'Model',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: 'gemma3-1b-gpu-custom',
|
||||
description:
|
||||
'The model to use for the classifier. Only tested on `gemma3-1b-gpu-custom`.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2583,9 +2532,7 @@ type InferSettings<T extends SettingsSchema> = {
|
||||
: T[K]['default']
|
||||
: T[K]['default'] extends boolean
|
||||
? boolean
|
||||
: T[K]['default'] extends string
|
||||
? string
|
||||
: T[K]['default'];
|
||||
: T[K]['default'];
|
||||
};
|
||||
|
||||
type InferMergedSettings<T extends SettingsSchema> = {
|
||||
@@ -2597,9 +2544,7 @@ type InferMergedSettings<T extends SettingsSchema> = {
|
||||
: T[K]['default']
|
||||
: T[K]['default'] extends boolean
|
||||
? boolean
|
||||
: T[K]['default'] extends string
|
||||
? string
|
||||
: T[K]['default'];
|
||||
: T[K]['default'];
|
||||
};
|
||||
|
||||
export type Settings = InferSettings<SettingsSchemaType>;
|
||||
|
||||
@@ -54,7 +54,6 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
Policy.setDisableWorkspacePolicies(false);
|
||||
// Default to MATCH for existing tests
|
||||
mockCheckIntegrity.mockResolvedValue({
|
||||
status: 'match',
|
||||
|
||||
@@ -1182,7 +1182,6 @@ describe('startInteractiveUI', () => {
|
||||
getProjectRoot: () => '/root',
|
||||
getScreenReader: () => false,
|
||||
getDebugMode: () => false,
|
||||
getUseAlternateBuffer: () => true,
|
||||
});
|
||||
const mockSettings = {
|
||||
merged: {
|
||||
@@ -1217,8 +1216,6 @@ describe('startInteractiveUI', () => {
|
||||
runExitCleanup: vi.fn(),
|
||||
registerSyncCleanup: vi.fn(),
|
||||
registerTelemetryConfig: vi.fn(),
|
||||
setupSignalHandlers: vi.fn(),
|
||||
setupTtyCheck: vi.fn(() => vi.fn()),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1325,8 +1322,7 @@ describe('startInteractiveUI', () => {
|
||||
|
||||
// Verify all startup tasks were called
|
||||
expect(getVersion).toHaveBeenCalledTimes(1);
|
||||
// 5 cleanups: mouseEvents, consolePatcher, lineWrapping, instance.unmount, and TTY check
|
||||
expect(registerCleanup).toHaveBeenCalledTimes(5);
|
||||
expect(registerCleanup).toHaveBeenCalledTimes(4);
|
||||
|
||||
// Verify cleanup handler is registered with unmount function
|
||||
const cleanupFn = vi.mocked(registerCleanup).mock.calls[0][0];
|
||||
|
||||
@@ -32,8 +32,6 @@ import {
|
||||
registerSyncCleanup,
|
||||
runExitCleanup,
|
||||
registerTelemetryConfig,
|
||||
setupSignalHandlers,
|
||||
setupTtyCheck,
|
||||
} from './utils/cleanup.js';
|
||||
import {
|
||||
cleanupToolOutputFiles,
|
||||
@@ -102,8 +100,8 @@ import { loadSandboxConfig } from './config/sandboxConfig.js';
|
||||
import { deleteSession, listSessions } from './utils/sessions.js';
|
||||
import { createPolicyUpdater } from './config/policy.js';
|
||||
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
|
||||
import { TerminalProvider } from './ui/contexts/TerminalContext.js';
|
||||
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
|
||||
import { TerminalProvider } from './ui/contexts/TerminalContext.js';
|
||||
import { OverflowProvider } from './ui/contexts/OverflowContext.js';
|
||||
|
||||
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
|
||||
@@ -196,7 +194,7 @@ export async function startInteractiveUI(
|
||||
// and the Ink alternate buffer mode requires line wrapping harmful to
|
||||
// screen readers.
|
||||
const useAlternateBuffer = shouldEnterAlternateScreen(
|
||||
isAlternateBufferEnabled(config),
|
||||
isAlternateBufferEnabled(settings),
|
||||
config.getScreenReader(),
|
||||
);
|
||||
const mouseEventsEnabled = useAlternateBuffer;
|
||||
@@ -321,8 +319,6 @@ export async function startInteractiveUI(
|
||||
});
|
||||
|
||||
registerCleanup(() => instance.unmount());
|
||||
|
||||
registerCleanup(setupTtyCheck());
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
@@ -344,8 +340,6 @@ export async function main() {
|
||||
|
||||
setupUnhandledRejectionHandler();
|
||||
|
||||
setupSignalHandlers();
|
||||
|
||||
const slashCommandConflictHandler = new SlashCommandConflictHandler();
|
||||
slashCommandConflictHandler.start();
|
||||
registerCleanup(() => slashCommandConflictHandler.stop());
|
||||
@@ -652,7 +646,10 @@ export async function main() {
|
||||
process.stdin.setRawMode(true);
|
||||
|
||||
// This cleanup isn't strictly needed but may help in certain situations.
|
||||
registerSyncCleanup(() => {
|
||||
process.on('SIGTERM', () => {
|
||||
process.stdin.setRawMode(wasRaw);
|
||||
});
|
||||
process.on('SIGINT', () => {
|
||||
process.stdin.setRawMode(wasRaw);
|
||||
});
|
||||
}
|
||||
@@ -678,7 +675,7 @@ export async function main() {
|
||||
|
||||
let input = config.getQuestion();
|
||||
const useAlternateBuffer = shouldEnterAlternateScreen(
|
||||
isAlternateBufferEnabled(config),
|
||||
isAlternateBufferEnabled(settings),
|
||||
config.getScreenReader(),
|
||||
);
|
||||
const rawStartupWarnings = await getStartupWarnings();
|
||||
|
||||
@@ -156,7 +156,6 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getExperiments: vi.fn().mockReturnValue(undefined),
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
validatePathAccess: vi.fn().mockReturnValue(null),
|
||||
getUseAlternateBuffer: vi.fn().mockReturnValue(false),
|
||||
...overrides,
|
||||
}) as unknown as Config;
|
||||
|
||||
|
||||
@@ -547,11 +547,6 @@ const baseMockUiState = {
|
||||
},
|
||||
hintMode: false,
|
||||
hintBuffer: '',
|
||||
bannerData: {
|
||||
defaultText: '',
|
||||
warningText: '',
|
||||
},
|
||||
bannerVisible: false,
|
||||
};
|
||||
|
||||
export const mockAppState: AppState = {
|
||||
@@ -703,21 +698,6 @@ export const renderWithProviders = (
|
||||
});
|
||||
}
|
||||
|
||||
// Wrap config in a Proxy so useAlternateBuffer hook (which reads from Config) gets the correct value,
|
||||
// without replacing the entire config object and its other values.
|
||||
let finalConfig = config;
|
||||
if (useAlternateBuffer !== undefined) {
|
||||
finalConfig = new Proxy(config, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === 'getUseAlternateBuffer') {
|
||||
return () => useAlternateBuffer;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const mainAreaWidth = terminalWidth;
|
||||
|
||||
const finalUiState = {
|
||||
@@ -746,7 +726,7 @@ export const renderWithProviders = (
|
||||
|
||||
const renderResult = render(
|
||||
<AppContext.Provider value={appState}>
|
||||
<ConfigContext.Provider value={finalConfig}>
|
||||
<ConfigContext.Provider value={config}>
|
||||
<SettingsContext.Provider value={finalSettings}>
|
||||
<UIStateContext.Provider value={finalUiState}>
|
||||
<VimModeProvider settings={finalSettings}>
|
||||
@@ -758,7 +738,7 @@ export const renderWithProviders = (
|
||||
<UIActionsContext.Provider value={finalUIActions}>
|
||||
<OverflowProvider>
|
||||
<ToolActionsProvider
|
||||
config={finalConfig}
|
||||
config={config}
|
||||
toolCalls={allToolCalls}
|
||||
>
|
||||
<AskUserActionsProvider
|
||||
|
||||
@@ -2675,10 +2675,6 @@ describe('AppContainer State Management', () => {
|
||||
isAlternateMode = false,
|
||||
childHandler?: Mock,
|
||||
) => {
|
||||
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(
|
||||
isAlternateMode,
|
||||
);
|
||||
|
||||
// Update settings for this test run
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const testSettings = {
|
||||
@@ -3368,8 +3364,6 @@ describe('AppContainer State Management', () => {
|
||||
);
|
||||
vi.mocked(checkPermissions).mockResolvedValue([]);
|
||||
|
||||
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
unmount = renderAppContainer({
|
||||
@@ -3602,8 +3596,6 @@ describe('AppContainer State Management', () => {
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({
|
||||
|
||||
@@ -145,6 +145,7 @@ import { useSessionResume } from './hooks/useSessionResume.js';
|
||||
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
|
||||
import { useSessionRetentionCheck } from './hooks/useSessionRetentionCheck.js';
|
||||
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
||||
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
|
||||
import { useSettings } from './contexts/SettingsContext.js';
|
||||
import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js';
|
||||
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
|
||||
@@ -227,7 +228,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
});
|
||||
|
||||
useMemoryMonitor(historyManager);
|
||||
const isAlternateBuffer = config.getUseAlternateBuffer();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const [corgiMode, setCorgiMode] = useState(false);
|
||||
const [forceRerenderKey, setForceRerenderKey] = useState(0);
|
||||
const [debugMessage, setDebugMessage] = useState<string>('');
|
||||
@@ -263,16 +264,14 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
() => isWorkspaceTrusted(settings.merged).isTrusted,
|
||||
);
|
||||
|
||||
const [queueErrorMessage, setQueueErrorMessage] = useTimedMessage<string>(
|
||||
QUEUE_ERROR_DISPLAY_DURATION_MS,
|
||||
const [queueErrorMessage, setQueueErrorMessage] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [newAgents, setNewAgents] = useState<AgentDefinition[] | null>(null);
|
||||
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
|
||||
const [expandHintTrigger, triggerExpandHint] = useTimedMessage<boolean>(
|
||||
EXPAND_HINT_DURATION_MS,
|
||||
);
|
||||
const showIsExpandableHint = Boolean(expandHintTrigger);
|
||||
const [showIsExpandableHint, setShowIsExpandableHint] = useState(false);
|
||||
const expandHintTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const overflowState = useOverflowState();
|
||||
const overflowingIdsSize = overflowState?.overflowingIds.size ?? 0;
|
||||
const hasOverflowState = overflowingIdsSize > 0 || !constrainHeight;
|
||||
@@ -285,15 +284,39 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
* boolean dependency (hasOverflowState) to ensure the timer only resets on
|
||||
* genuine state transitions, preventing it from infinitely resetting during
|
||||
* active text streaming.
|
||||
*
|
||||
* In alternate buffer mode, we don't trigger the hint automatically on overflow
|
||||
* to avoid noise, but the user can still trigger it manually with Ctrl+O.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (hasOverflowState && !isAlternateBuffer) {
|
||||
triggerExpandHint(true);
|
||||
if (isAlternateBuffer) {
|
||||
setShowIsExpandableHint(false);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [hasOverflowState, isAlternateBuffer, triggerExpandHint]);
|
||||
|
||||
if (hasOverflowState) {
|
||||
setShowIsExpandableHint(true);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
expandHintTimerRef.current = setTimeout(() => {
|
||||
setShowIsExpandableHint(false);
|
||||
}, EXPAND_HINT_DURATION_MS);
|
||||
}
|
||||
}, [hasOverflowState, isAlternateBuffer, constrainHeight]);
|
||||
|
||||
/**
|
||||
* Safe cleanup to ensure the expansion hint timer is cancelled when the
|
||||
* component unmounts, preventing memory leaks.
|
||||
*/
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const [defaultBannerText, setDefaultBannerText] = useState('');
|
||||
const [warningBannerText, setWarningBannerText] = useState('');
|
||||
@@ -544,7 +567,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } =
|
||||
useConsoleMessages();
|
||||
|
||||
const mainAreaWidth = calculateMainAreaWidth(terminalWidth, config);
|
||||
const mainAreaWidth = calculateMainAreaWidth(terminalWidth, settings);
|
||||
// Derive widths for InputPrompt using shared helper
|
||||
const { inputWidth, suggestionsWidth } = useMemo(() => {
|
||||
const { inputWidth, suggestionsWidth } =
|
||||
@@ -1229,7 +1252,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
async (submittedValue: string) => {
|
||||
reset();
|
||||
// Explicitly hide the expansion hint and clear its x-second timer when a new turn begins.
|
||||
triggerExpandHint(null);
|
||||
setShowIsExpandableHint(false);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
if (!constrainHeight) {
|
||||
setConstrainHeight(true);
|
||||
if (!isAlternateBuffer) {
|
||||
@@ -1301,14 +1327,16 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
refreshStatic,
|
||||
reset,
|
||||
handleHintSubmit,
|
||||
triggerExpandHint,
|
||||
],
|
||||
);
|
||||
|
||||
const handleClearScreen = useCallback(() => {
|
||||
reset();
|
||||
// Explicitly hide the expansion hint and clear its x-second timer when clearing the screen.
|
||||
triggerExpandHint(null);
|
||||
setShowIsExpandableHint(false);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
historyManager.clearItems();
|
||||
clearConsoleMessagesState();
|
||||
refreshStatic();
|
||||
@@ -1317,7 +1345,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
clearConsoleMessagesState,
|
||||
refreshStatic,
|
||||
reset,
|
||||
triggerExpandHint,
|
||||
setShowIsExpandableHint,
|
||||
]);
|
||||
|
||||
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
|
||||
@@ -1604,6 +1632,17 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}
|
||||
}, [ideNeedsRestart]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queueErrorMessage) {
|
||||
const timer = setTimeout(() => {
|
||||
setQueueErrorMessage(null);
|
||||
}, QUEUE_ERROR_DISPLAY_DURATION_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
return undefined;
|
||||
}, [queueErrorMessage, setQueueErrorMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
@@ -1709,7 +1748,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
setConstrainHeight(true);
|
||||
if (keyMatchers[Command.SHOW_MORE_LINES](key)) {
|
||||
// If the user manually collapses the view, show the hint and reset the x-second timer.
|
||||
triggerExpandHint(true);
|
||||
setShowIsExpandableHint(true);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
expandHintTimerRef.current = setTimeout(() => {
|
||||
setShowIsExpandableHint(false);
|
||||
}, EXPAND_HINT_DURATION_MS);
|
||||
}
|
||||
if (!isAlternateBuffer) {
|
||||
refreshStatic();
|
||||
@@ -1758,7 +1803,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
) {
|
||||
setConstrainHeight(false);
|
||||
// If the user manually expands the view, show the hint and reset the x-second timer.
|
||||
triggerExpandHint(true);
|
||||
setShowIsExpandableHint(true);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
expandHintTimerRef.current = setTimeout(() => {
|
||||
setShowIsExpandableHint(false);
|
||||
}, EXPAND_HINT_DURATION_MS);
|
||||
if (!isAlternateBuffer) {
|
||||
refreshStatic();
|
||||
}
|
||||
@@ -1863,7 +1914,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
showTransientMessage,
|
||||
settings.merged.general.devtools,
|
||||
showErrorDetails,
|
||||
triggerExpandHint,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { act, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { useEffect } from 'react';
|
||||
import { Composer } from './Composer.js';
|
||||
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
|
||||
import {
|
||||
@@ -34,7 +34,6 @@ import { StreamingState } from '../types.js';
|
||||
import { TransientMessageType } from '../../utils/events.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import type { TextBuffer } from './shared/text-buffer.js';
|
||||
|
||||
const composerTestControls = vi.hoisted(() => ({
|
||||
suggestionsVisible: false,
|
||||
@@ -264,26 +263,16 @@ const renderComposer = async (
|
||||
</ConfigContext.Provider>,
|
||||
);
|
||||
await result.waitUntilReady();
|
||||
|
||||
// Wait for shortcuts hint debounce if using fake timers
|
||||
if (vi.isFakeTimers()) {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
describe('Composer', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
composerTestControls.suggestionsVisible = false;
|
||||
composerTestControls.isAlternateBuffer = false;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -402,7 +391,7 @@ describe('Composer', () => {
|
||||
expect(output).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('renders LoadingIndicator with thought when loadingPhrases is off', async () => {
|
||||
it('renders LoadingIndicator without thought when loadingPhrases is off', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
thought: { subject: 'Hidden', description: 'Should not show' },
|
||||
@@ -415,7 +404,7 @@ describe('Composer', () => {
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator');
|
||||
expect(output).toContain('LoadingIndicator: Hidden');
|
||||
expect(output).not.toContain('Should not show');
|
||||
});
|
||||
|
||||
it('does not render LoadingIndicator when waiting for confirmation', async () => {
|
||||
@@ -820,28 +809,6 @@ describe('Composer', () => {
|
||||
});
|
||||
|
||||
describe('Shortcuts Hint', () => {
|
||||
it('restores shortcuts hint after 200ms debounce when buffer is empty', async () => {
|
||||
const { lastFrame } = await renderComposer(
|
||||
createMockUIState({
|
||||
buffer: { text: '' } as unknown as TextBuffer,
|
||||
cleanUiDetailsVisible: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(lastFrame({ allowEmpty: true })).toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('does not show shortcuts hint immediately when buffer has text', async () => {
|
||||
const uiState = createMockUIState({
|
||||
buffer: { text: 'hello' } as unknown as TextBuffer,
|
||||
cleanUiDetailsVisible: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint when showShortcutsHint setting is false', async () => {
|
||||
const uiState = createMockUIState();
|
||||
const settings = createMockSettings({
|
||||
@@ -890,27 +857,6 @@ describe('Composer', () => {
|
||||
expect(lastFrame()).toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint while loading when full UI details are visible', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint when text is typed in buffer', async () => {
|
||||
const uiState = createMockUIState({
|
||||
buffer: { text: 'hello' } as unknown as TextBuffer,
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint while loading in minimal mode', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
@@ -984,10 +930,9 @@ describe('Composer', () => {
|
||||
streamingState: StreamingState.Idle,
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = await renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('ShortcutsHelp');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides shortcuts help while streaming', async () => {
|
||||
@@ -996,10 +941,9 @@ describe('Composer', () => {
|
||||
streamingState: StreamingState.Responding,
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = await renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides shortcuts help when action is required', async () => {
|
||||
@@ -1012,10 +956,9 @@ describe('Composer', () => {
|
||||
),
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = await renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -151,30 +151,11 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
: undefined,
|
||||
);
|
||||
const hideShortcutsHintForSuggestions = hideUiDetailsForSuggestions;
|
||||
const isModelIdle = uiState.streamingState === StreamingState.Idle;
|
||||
const isBufferEmpty = uiState.buffer.text.length === 0;
|
||||
const canShowShortcutsHint =
|
||||
isModelIdle && isBufferEmpty && !hasPendingActionRequired;
|
||||
const [showShortcutsHintDebounced, setShowShortcutsHintDebounced] =
|
||||
useState(canShowShortcutsHint);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canShowShortcutsHint) {
|
||||
setShowShortcutsHintDebounced(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setShowShortcutsHintDebounced(true);
|
||||
}, 200);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [canShowShortcutsHint]);
|
||||
|
||||
const showShortcutsHint =
|
||||
settings.merged.ui.showShortcutsHint &&
|
||||
!hideShortcutsHintForSuggestions &&
|
||||
showShortcutsHintDebounced;
|
||||
!hideMinimalModeHintWhileBusy &&
|
||||
!hasPendingActionRequired;
|
||||
const showMinimalModeBleedThrough =
|
||||
!hideUiDetailsForSuggestions && Boolean(minimalModeBleedThrough);
|
||||
const showMinimalInlineLoading = !showUiDetails && showLoadingIndicator;
|
||||
@@ -229,7 +210,8 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
inline
|
||||
thought={
|
||||
uiState.streamingState ===
|
||||
StreamingState.WaitingForConfirmation
|
||||
StreamingState.WaitingForConfirmation ||
|
||||
settings.merged.ui.loadingPhrases === 'off'
|
||||
? undefined
|
||||
: uiState.thought
|
||||
}
|
||||
@@ -272,7 +254,8 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
inline
|
||||
thought={
|
||||
uiState.streamingState ===
|
||||
StreamingState.WaitingForConfirmation
|
||||
StreamingState.WaitingForConfirmation ||
|
||||
settings.merged.ui.loadingPhrases === 'off'
|
||||
? undefined
|
||||
: uiState.thought
|
||||
}
|
||||
|
||||
@@ -135,7 +135,6 @@ export const DialogManager = ({
|
||||
isModelNotFoundError={
|
||||
!!uiState.quota.proQuotaRequest.isModelNotFoundError
|
||||
}
|
||||
authType={uiState.quota.proQuotaRequest.authType}
|
||||
onChoice={uiActions.handleProQuotaChoice}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -167,7 +167,6 @@ Implement a comprehensive authentication system with multiple providers.
|
||||
readTextFile: vi.fn(),
|
||||
writeTextFile: vi.fn(),
|
||||
}),
|
||||
getUseAlternateBuffer: () => options?.useAlternateBuffer ?? true,
|
||||
} as unknown as import('@google/gemini-cli-core').Config,
|
||||
},
|
||||
);
|
||||
@@ -444,7 +443,6 @@ Implement a comprehensive authentication system with multiple providers.
|
||||
readTextFile: vi.fn(),
|
||||
writeTextFile: vi.fn(),
|
||||
}),
|
||||
getUseAlternateBuffer: () => useAlternateBuffer ?? true,
|
||||
} as unknown as import('@google/gemini-cli-core').Config,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -13,7 +13,6 @@ import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import {
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
AuthType,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the child component to make it easier to test the parent
|
||||
@@ -63,7 +62,7 @@ describe('ProQuotaDialog', () => {
|
||||
|
||||
describe('for non-flash model failures', () => {
|
||||
describe('when it is a terminal quota error', () => {
|
||||
it('should render switch, upgrade, and stop options for LOGIN_WITH_GOOGLE', () => {
|
||||
it('should render switch, upgrade, and stop options for paid tiers', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
@@ -71,7 +70,6 @@ describe('ProQuotaDialog', () => {
|
||||
message="paid tier quota error"
|
||||
isTerminalQuotaError={true}
|
||||
isModelNotFoundError={false}
|
||||
authType={AuthType.LOGIN_WITH_GOOGLE}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
@@ -101,39 +99,6 @@ describe('ProQuotaDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT render upgrade option for USE_GEMINI', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
message="paid tier quota error"
|
||||
isTerminalQuotaError={true}
|
||||
isModelNotFoundError={false}
|
||||
authType={AuthType.USE_GEMINI}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(RadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
{
|
||||
label: 'Switch to gemini-2.5-flash',
|
||||
value: 'retry_always',
|
||||
key: 'retry_always',
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
value: 'retry_later',
|
||||
key: 'retry_later',
|
||||
},
|
||||
],
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render "Keep trying" and "Stop" options when failed model and fallback model are the same', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
@@ -165,7 +130,7 @@ describe('ProQuotaDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render switch, upgrade, and stop options for LOGIN_WITH_GOOGLE (free tier)', () => {
|
||||
it('should render switch, upgrade, and stop options for free tier', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
@@ -173,7 +138,6 @@ describe('ProQuotaDialog', () => {
|
||||
message="free tier quota error"
|
||||
isTerminalQuotaError={true}
|
||||
isModelNotFoundError={false}
|
||||
authType={AuthType.LOGIN_WITH_GOOGLE}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
@@ -240,7 +204,7 @@ describe('ProQuotaDialog', () => {
|
||||
});
|
||||
|
||||
describe('when it is a model not found error', () => {
|
||||
it('should render switch, upgrade, and stop options for LOGIN_WITH_GOOGLE', () => {
|
||||
it('should render switch and stop options regardless of tier', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-3-pro-preview"
|
||||
@@ -248,7 +212,6 @@ describe('ProQuotaDialog', () => {
|
||||
message="You don't have access to gemini-3-pro-preview yet."
|
||||
isTerminalQuotaError={false}
|
||||
isModelNotFoundError={true}
|
||||
authType={AuthType.LOGIN_WITH_GOOGLE}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
@@ -278,7 +241,7 @@ describe('ProQuotaDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT render upgrade option for USE_GEMINI', () => {
|
||||
it('should render switch and stop options for paid tier as well', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-3-pro-preview"
|
||||
@@ -286,7 +249,6 @@ describe('ProQuotaDialog', () => {
|
||||
message="You don't have access to gemini-3-pro-preview yet."
|
||||
isTerminalQuotaError={false}
|
||||
isModelNotFoundError={true}
|
||||
authType={AuthType.USE_GEMINI}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
@@ -299,6 +261,11 @@ describe('ProQuotaDialog', () => {
|
||||
value: 'retry_always',
|
||||
key: 'retry_always',
|
||||
},
|
||||
{
|
||||
label: 'Upgrade for higher limits',
|
||||
value: 'upgrade',
|
||||
key: 'upgrade',
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
value: 'retry_later',
|
||||
|
||||
@@ -8,7 +8,6 @@ import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { AuthType } from '@google/gemini-cli-core';
|
||||
|
||||
interface ProQuotaDialogProps {
|
||||
failedModel: string;
|
||||
@@ -16,7 +15,6 @@ interface ProQuotaDialogProps {
|
||||
message: string;
|
||||
isTerminalQuotaError: boolean;
|
||||
isModelNotFoundError?: boolean;
|
||||
authType?: AuthType;
|
||||
onChoice: (
|
||||
choice: 'retry_later' | 'retry_once' | 'retry_always' | 'upgrade',
|
||||
) => void;
|
||||
@@ -28,7 +26,6 @@ export function ProQuotaDialog({
|
||||
message,
|
||||
isTerminalQuotaError,
|
||||
isModelNotFoundError,
|
||||
authType,
|
||||
onChoice,
|
||||
}: ProQuotaDialogProps): React.JSX.Element {
|
||||
let items;
|
||||
@@ -54,15 +51,11 @@ export function ProQuotaDialog({
|
||||
value: 'retry_always' as const,
|
||||
key: 'retry_always',
|
||||
},
|
||||
...(authType === AuthType.LOGIN_WITH_GOOGLE
|
||||
? [
|
||||
{
|
||||
label: 'Upgrade for higher limits',
|
||||
value: 'upgrade' as const,
|
||||
key: 'upgrade',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'Upgrade for higher limits',
|
||||
value: 'upgrade' as const,
|
||||
key: 'upgrade',
|
||||
},
|
||||
{
|
||||
label: `Stop`,
|
||||
value: 'retry_later' as const,
|
||||
|
||||
@@ -187,9 +187,7 @@ describe('ToastDisplay', () => {
|
||||
constrainHeight: true,
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'Ctrl+O to show more lines of the last response',
|
||||
);
|
||||
expect(lastFrame()).toContain('Press Ctrl+O to show more lines');
|
||||
});
|
||||
|
||||
it('renders collapse hint when showIsExpandableHint is true and constrainHeight is false', async () => {
|
||||
@@ -198,8 +196,6 @@ describe('ToastDisplay', () => {
|
||||
constrainHeight: false,
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'Ctrl+O to collapse lines of the last response',
|
||||
);
|
||||
expect(lastFrame()).toContain('Press Ctrl+O to collapse lines');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ export const ToastDisplay: React.FC = () => {
|
||||
const action = uiState.constrainHeight ? 'show more' : 'collapse';
|
||||
return (
|
||||
<Text color={theme.text.accent}>
|
||||
Ctrl+O to {action} lines of the last response
|
||||
Press Ctrl+O to {action} lines for the most recent response
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ describe('ToolConfirmationQueue', () => {
|
||||
storage: {
|
||||
getPlansDir: () => '/mock/temp/plans',
|
||||
},
|
||||
getUseAlternateBuffer: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -228,7 +227,7 @@ describe('ToolConfirmationQueue', () => {
|
||||
// availableContentHeight = Math.max(9 - 6, 4) = 4
|
||||
// MaxSizedBox in ToolConfirmationMessage will use 4
|
||||
// It should show truncation message
|
||||
await waitFor(() => expect(lastFrame()).toContain('49 hidden (Ctrl+O)'));
|
||||
await waitFor(() => expect(lastFrame()).toContain('first 49 lines hidden'));
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ Footer
|
||||
`;
|
||||
|
||||
exports[`Composer > Snapshots > matches snapshot while streaming 1`] = `
|
||||
" LoadingIndicator: Thinking
|
||||
" LoadingIndicator: Thinking ShortcutsHint
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
ApprovalModeIndicator
|
||||
InputPrompt: Type your message or @path/to/file
|
||||
|
||||
@@ -74,7 +74,7 @@ Implementation Steps
|
||||
6. Add LDAP provider support in src/auth/providers/LDAPProvider.ts
|
||||
7. Create token refresh mechanism in src/auth/TokenManager.ts
|
||||
8. Add multi-factor authentication in src/auth/MFAService.ts
|
||||
... last 22 lines hidden (Ctrl+O to show) ...
|
||||
... last 22 lines hidden ...
|
||||
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
|
||||
@@ -112,7 +112,7 @@ exports[`<HistoryItemDisplay /> > gemini items (alternateBuffer=false) > should
|
||||
|
||||
exports[`<HistoryItemDisplay /> > gemini items (alternateBuffer=false) > should render a truncated gemini item 1`] = `
|
||||
"✦ Example code block:
|
||||
... 42 hidden (Ctrl+O) ...
|
||||
... first 42 lines hidden ...
|
||||
43 Line 43
|
||||
44 Line 44
|
||||
45 Line 45
|
||||
@@ -126,7 +126,7 @@ exports[`<HistoryItemDisplay /> > gemini items (alternateBuffer=false) > should
|
||||
|
||||
exports[`<HistoryItemDisplay /> > gemini items (alternateBuffer=false) > should render a truncated gemini_content item 1`] = `
|
||||
" Example code block:
|
||||
... 42 hidden (Ctrl+O) ...
|
||||
... first 42 lines hidden ...
|
||||
43 Line 43
|
||||
44 Line 44
|
||||
45 Line 45
|
||||
|
||||
@@ -49,7 +49,7 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Con
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊷ Shell Command Running a long command... │
|
||||
│ │
|
||||
│ ... first 11 lines hidden (Ctrl+O to show) ... │
|
||||
│ ... first 11 lines hidden ... │
|
||||
│ Line 12 │
|
||||
│ Line 13 │
|
||||
│ Line 14 │
|
||||
|
||||
@@ -6,7 +6,7 @@ exports[`ToolConfirmationQueue > calculates availableContentHeight based on avai
|
||||
│ │
|
||||
│ ? replace edit file │
|
||||
│ │
|
||||
│ ... 49 hidden (Ctrl+O) ... │
|
||||
│ ... first 49 lines hidden ... │
|
||||
│ 50 line │
|
||||
│ Apply this change? │
|
||||
│ │
|
||||
@@ -96,7 +96,7 @@ exports[`ToolConfirmationQueue > renders expansion hint when content is long and
|
||||
│ │
|
||||
│ ? replace edit file │
|
||||
│ │
|
||||
│ ... 49 hidden (Ctrl+O) ... │
|
||||
│ ... first 49 lines hidden ... │
|
||||
│ 50 line │
|
||||
│ Apply this change? │
|
||||
│ │
|
||||
|
||||
@@ -106,7 +106,7 @@ describe('ToolOverflowConsistencyChecks: ToolGroupMessage and ToolResultDisplay
|
||||
);
|
||||
|
||||
// Verify truncation is occurring (standard mode uses MaxSizedBox)
|
||||
await waitFor(() => expect(lastFrame()).toContain('hidden (Ctrl+O'));
|
||||
await waitFor(() => expect(lastFrame()).toContain('hidden ...'));
|
||||
|
||||
// In Standard mode, ToolGroupMessage calculates hasOverflow correctly now.
|
||||
// While Standard mode doesn't render the inline hint (ShowMoreLines returns null),
|
||||
|
||||
@@ -8,6 +8,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import { ToolGroupMessage } from './ToolGroupMessage.js';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { StreamingState, type IndividualToolCallDisplay } from '../../types.js';
|
||||
import { OverflowProvider } from '../../contexts/OverflowContext.js';
|
||||
import { waitFor } from '../../../test-utils/async.js';
|
||||
import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -31,14 +32,16 @@ describe('ToolResultDisplay Overflow', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
item={{ id: 1, type: 'tool_group', tools: toolCalls }}
|
||||
toolCalls={toolCalls}
|
||||
availableTerminalHeight={15} // Small height to force overflow
|
||||
terminalWidth={80}
|
||||
isExpandable={true}
|
||||
/>,
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<OverflowProvider>
|
||||
<ToolGroupMessage
|
||||
item={{ id: 1, type: 'tool_group', tools: toolCalls }}
|
||||
toolCalls={toolCalls}
|
||||
availableTerminalHeight={15} // Small height to force overflow
|
||||
terminalWidth={80}
|
||||
isExpandable={true}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
{
|
||||
uiState: {
|
||||
streamingState: StreamingState.Idle,
|
||||
@@ -48,13 +51,12 @@ describe('ToolResultDisplay Overflow', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
|
||||
// ResizeObserver might take a tick, though ToolGroupMessage calculates overflow synchronously
|
||||
await waitFor(() => {
|
||||
const frame = lastFrame();
|
||||
expect(frame.toLowerCase()).toContain('press ctrl+o to show more lines');
|
||||
});
|
||||
// ResizeObserver might take a tick
|
||||
await waitFor(() =>
|
||||
expect(lastFrame()?.toLowerCase()).toContain(
|
||||
'press ctrl+o to show more lines',
|
||||
),
|
||||
);
|
||||
|
||||
const frame = lastFrame();
|
||||
expect(frame).toBeDefined();
|
||||
|
||||
@@ -10,7 +10,7 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 30 and height 6 1`] = `
|
||||
"... 10 hidden (Ctrl+O) ...
|
||||
"... first 10 lines hidden ...
|
||||
'test';
|
||||
21 + const anotherNew =
|
||||
'test';
|
||||
@@ -20,7 +20,7 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 80 and height 6 1`] = `
|
||||
"... first 4 lines hidden (Ctrl+O to show) ...
|
||||
"... first 4 lines hidden ...
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
20 console.log('second hunk');
|
||||
21 - const anotherOld = 'test';
|
||||
@@ -103,7 +103,7 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 30 and height 6 1`] = `
|
||||
"... 10 hidden (Ctrl+O) ...
|
||||
"... first 10 lines hidden ...
|
||||
'test';
|
||||
21 + const anotherNew =
|
||||
'test';
|
||||
@@ -113,7 +113,7 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 80 and height 6 1`] = `
|
||||
"... first 4 lines hidden (Ctrl+O to show) ...
|
||||
"... first 4 lines hidden ...
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
20 console.log('second hunk');
|
||||
21 - const anotherOld = 'test';
|
||||
|
||||
@@ -37,7 +37,7 @@ exports[`ToolResultDisplay > renders string result as plain text when renderOutp
|
||||
`;
|
||||
|
||||
exports[`ToolResultDisplay > truncates very long string results 1`] = `
|
||||
"... 248 hidden (Ctrl+O) ...
|
||||
"... first 248 lines hidden ...
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
|
||||
@@ -41,9 +41,7 @@ describe('<MaxSizedBox />', () => {
|
||||
</OverflowProvider>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'... first 2 lines hidden (Ctrl+O to show) ...',
|
||||
);
|
||||
expect(lastFrame()).toContain('... first 2 lines hidden ...');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -61,9 +59,7 @@ describe('<MaxSizedBox />', () => {
|
||||
</OverflowProvider>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'... last 2 lines hidden (Ctrl+O to show) ...',
|
||||
);
|
||||
expect(lastFrame()).toContain('... last 2 lines hidden ...');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -81,9 +77,7 @@ describe('<MaxSizedBox />', () => {
|
||||
</OverflowProvider>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'... first 2 lines hidden (Ctrl+O to show) ...',
|
||||
);
|
||||
expect(lastFrame()).toContain('... first 2 lines hidden ...');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -99,9 +93,7 @@ describe('<MaxSizedBox />', () => {
|
||||
</OverflowProvider>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'... first 1 line hidden (Ctrl+O to show) ...',
|
||||
);
|
||||
expect(lastFrame()).toContain('... first 1 line hidden ...');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -119,9 +111,7 @@ describe('<MaxSizedBox />', () => {
|
||||
</OverflowProvider>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'... first 7 lines hidden (Ctrl+O to show) ...',
|
||||
);
|
||||
expect(lastFrame()).toContain('... first 7 lines hidden ...');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -207,9 +197,7 @@ describe('<MaxSizedBox />', () => {
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'... first 21 lines hidden (Ctrl+O to show) ...',
|
||||
);
|
||||
expect(lastFrame()).toContain('... first 21 lines hidden ...');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -230,9 +218,7 @@ describe('<MaxSizedBox />', () => {
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'... last 21 lines hidden (Ctrl+O to show) ...',
|
||||
);
|
||||
expect(lastFrame()).toContain('... last 21 lines hidden ...');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -261,9 +247,7 @@ describe('<MaxSizedBox />', () => {
|
||||
const lastLine = lines[lines.length - 1];
|
||||
|
||||
// The last line should only contain the hidden indicator, no leaked content
|
||||
expect(lastLine).toMatch(
|
||||
/^\.\.\. last \d+ lines? hidden \(Ctrl\+O to show\) \.\.\.$/,
|
||||
);
|
||||
expect(lastLine).toMatch(/^\.\.\. last \d+ lines? hidden \.\.\.$/);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -9,9 +9,6 @@ import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
||||
import { Box, Text, ResizeObserver, type DOMElement } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { useOverflowActions } from '../../contexts/OverflowContext.js';
|
||||
import { isNarrowWidth } from '../../utils/isNarrowWidth.js';
|
||||
import { Command } from '../../../config/keyBindings.js';
|
||||
import { formatCommand } from '../../utils/keybindingUtils.js';
|
||||
|
||||
/**
|
||||
* Minimum height for the MaxSizedBox component.
|
||||
@@ -87,9 +84,6 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
|
||||
|
||||
const totalHiddenLines = hiddenLinesCount + additionalHiddenLinesCount;
|
||||
|
||||
const isNarrow = maxWidth !== undefined && isNarrowWidth(maxWidth);
|
||||
const showMoreKey = formatCommand(Command.SHOW_MORE_LINES);
|
||||
|
||||
useEffect(() => {
|
||||
if (totalHiddenLines > 0) {
|
||||
addOverflowingId?.(id);
|
||||
@@ -122,9 +116,8 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
|
||||
>
|
||||
{totalHiddenLines > 0 && overflowDirection === 'top' && (
|
||||
<Text color={theme.text.secondary} wrap="truncate">
|
||||
{isNarrow
|
||||
? `... ${totalHiddenLines} hidden (${showMoreKey}) ...`
|
||||
: `... first ${totalHiddenLines} line${totalHiddenLines === 1 ? '' : 's'} hidden (${showMoreKey} to show) ...`}
|
||||
... first {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '}
|
||||
hidden ...
|
||||
</Text>
|
||||
)}
|
||||
<Box
|
||||
@@ -144,9 +137,8 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
|
||||
</Box>
|
||||
{totalHiddenLines > 0 && overflowDirection === 'bottom' && (
|
||||
<Text color={theme.text.secondary} wrap="truncate">
|
||||
{isNarrow
|
||||
? `... ${totalHiddenLines} hidden (${showMoreKey}) ...`
|
||||
: `... last ${totalHiddenLines} line${totalHiddenLines === 1 ? '' : 's'} hidden (${showMoreKey} to show) ...`}
|
||||
... last {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '}
|
||||
hidden ...
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -6,11 +6,20 @@
|
||||
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { Scrollable } from './Scrollable.js';
|
||||
import { Text, Box } from 'ink';
|
||||
import { Text } from 'ink';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as ScrollProviderModule from '../../contexts/ScrollProvider.js';
|
||||
import { act } from 'react';
|
||||
import { waitFor } from '../../../test-utils/async.js';
|
||||
|
||||
vi.mock('ink', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('ink')>();
|
||||
return {
|
||||
...actual,
|
||||
getInnerHeight: vi.fn(() => 5),
|
||||
getScrollHeight: vi.fn(() => 10),
|
||||
getBoundingBox: vi.fn(() => ({ x: 0, y: 0, width: 10, height: 5 })),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../hooks/useAnimatedScrollbar.js', () => ({
|
||||
useAnimatedScrollbar: (
|
||||
@@ -120,26 +129,20 @@ describe('<Scrollable />', () => {
|
||||
</Scrollable>,
|
||||
);
|
||||
await waitUntilReady2();
|
||||
await waitFor(() => {
|
||||
expect(capturedEntry?.getScrollState().scrollTop).toBe(5);
|
||||
});
|
||||
expect(capturedEntry.getScrollState().scrollTop).toBe(5);
|
||||
|
||||
// Call scrollBy multiple times (upwards) in the same tick
|
||||
await act(async () => {
|
||||
capturedEntry?.scrollBy(-1);
|
||||
capturedEntry?.scrollBy(-1);
|
||||
capturedEntry!.scrollBy(-1);
|
||||
capturedEntry!.scrollBy(-1);
|
||||
});
|
||||
// Should have moved up by 2 (5 -> 3)
|
||||
await waitFor(() => {
|
||||
expect(capturedEntry?.getScrollState().scrollTop).toBe(3);
|
||||
});
|
||||
expect(capturedEntry.getScrollState().scrollTop).toBe(3);
|
||||
|
||||
await act(async () => {
|
||||
capturedEntry?.scrollBy(-2);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedEntry?.getScrollState().scrollTop).toBe(1);
|
||||
capturedEntry!.scrollBy(-2);
|
||||
});
|
||||
expect(capturedEntry.getScrollState().scrollTop).toBe(1);
|
||||
unmount2();
|
||||
});
|
||||
|
||||
@@ -188,6 +191,10 @@ describe('<Scrollable />', () => {
|
||||
keySequence,
|
||||
expectedScrollTop,
|
||||
}) => {
|
||||
// Dynamically import ink to mock getScrollHeight
|
||||
const ink = await import('ink');
|
||||
vi.mocked(ink.getScrollHeight).mockReturnValue(scrollHeight);
|
||||
|
||||
let capturedEntry: ScrollProviderModule.ScrollableEntry | undefined;
|
||||
vi.spyOn(ScrollProviderModule, 'useScrollable').mockImplementation(
|
||||
async (entry, isActive) => {
|
||||
@@ -199,9 +206,7 @@ describe('<Scrollable />', () => {
|
||||
|
||||
const { stdin, waitUntilReady, unmount } = renderWithProviders(
|
||||
<Scrollable hasFocus={true} height={5}>
|
||||
<Box height={scrollHeight}>
|
||||
<Text>Content</Text>
|
||||
</Box>
|
||||
<Text>Content</Text>
|
||||
</Scrollable>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
@@ -4,9 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
|
||||
import { Box, ResizeObserver, type DOMElement } from 'ink';
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { Box, getInnerHeight, getScrollHeight, type DOMElement } from 'ink';
|
||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||
import { useScrollable } from '../../contexts/ScrollProvider.js';
|
||||
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
||||
@@ -35,101 +41,62 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
||||
flexGrow,
|
||||
}) => {
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
const viewportRef = useRef<DOMElement | null>(null);
|
||||
const contentRef = useRef<DOMElement | null>(null);
|
||||
const ref = useRef<DOMElement>(null);
|
||||
const [size, setSize] = useState({
|
||||
innerHeight: typeof height === 'number' ? height : 0,
|
||||
innerHeight: 0,
|
||||
scrollHeight: 0,
|
||||
});
|
||||
const sizeRef = useRef(size);
|
||||
const scrollTopRef = useRef(scrollTop);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
sizeRef.current = size;
|
||||
}, [size]);
|
||||
|
||||
const childrenCountRef = useRef(0);
|
||||
|
||||
// This effect needs to run on every render to correctly measure the container
|
||||
// and scroll to the bottom if new children are added.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useLayoutEffect(() => {
|
||||
scrollTopRef.current = scrollTop;
|
||||
}, [scrollTop]);
|
||||
|
||||
const viewportObserverRef = useRef<ResizeObserver | null>(null);
|
||||
const contentObserverRef = useRef<ResizeObserver | null>(null);
|
||||
|
||||
const viewportRefCallback = useCallback((node: DOMElement | null) => {
|
||||
viewportObserverRef.current?.disconnect();
|
||||
viewportRef.current = node;
|
||||
|
||||
if (node) {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
const innerHeight = Math.round(entry.contentRect.height);
|
||||
setSize((prev) => {
|
||||
const scrollHeight = prev.scrollHeight;
|
||||
const isAtBottom =
|
||||
scrollHeight > prev.innerHeight &&
|
||||
scrollTopRef.current >= scrollHeight - prev.innerHeight - 1;
|
||||
|
||||
if (isAtBottom) {
|
||||
setScrollTop(Number.MAX_SAFE_INTEGER);
|
||||
}
|
||||
return { ...prev, innerHeight };
|
||||
});
|
||||
}
|
||||
});
|
||||
observer.observe(node);
|
||||
viewportObserverRef.current = observer;
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
const innerHeight = Math.round(getInnerHeight(ref.current));
|
||||
const scrollHeight = Math.round(getScrollHeight(ref.current));
|
||||
|
||||
const contentRefCallback = useCallback(
|
||||
(node: DOMElement | null) => {
|
||||
contentObserverRef.current?.disconnect();
|
||||
contentRef.current = node;
|
||||
const isAtBottom =
|
||||
scrollHeight > innerHeight && scrollTop >= scrollHeight - innerHeight - 1;
|
||||
|
||||
if (node) {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
const scrollHeight = Math.round(entry.contentRect.height);
|
||||
setSize((prev) => {
|
||||
const innerHeight = prev.innerHeight;
|
||||
const isAtBottom =
|
||||
prev.scrollHeight > innerHeight &&
|
||||
scrollTopRef.current >= prev.scrollHeight - innerHeight - 1;
|
||||
|
||||
if (
|
||||
isAtBottom ||
|
||||
(scrollToBottom && scrollHeight > prev.scrollHeight)
|
||||
) {
|
||||
setScrollTop(Number.MAX_SAFE_INTEGER);
|
||||
}
|
||||
return { ...prev, scrollHeight };
|
||||
});
|
||||
}
|
||||
});
|
||||
observer.observe(node);
|
||||
contentObserverRef.current = observer;
|
||||
if (
|
||||
size.innerHeight !== innerHeight ||
|
||||
size.scrollHeight !== scrollHeight
|
||||
) {
|
||||
setSize({ innerHeight, scrollHeight });
|
||||
if (isAtBottom) {
|
||||
setScrollTop(Math.max(0, scrollHeight - innerHeight));
|
||||
}
|
||||
},
|
||||
[scrollToBottom],
|
||||
);
|
||||
}
|
||||
|
||||
const childCountCurrent = React.Children.count(children);
|
||||
if (scrollToBottom && childrenCountRef.current !== childCountCurrent) {
|
||||
setScrollTop(Math.max(0, scrollHeight - innerHeight));
|
||||
}
|
||||
childrenCountRef.current = childCountCurrent;
|
||||
});
|
||||
|
||||
const { getScrollTop, setPendingScrollTop } = useBatchedScroll(scrollTop);
|
||||
|
||||
const scrollBy = useCallback(
|
||||
(delta: number) => {
|
||||
const { scrollHeight, innerHeight } = sizeRef.current;
|
||||
const maxScroll = Math.max(0, scrollHeight - innerHeight);
|
||||
const current = Math.min(getScrollTop(), maxScroll);
|
||||
let next = Math.max(0, current + delta);
|
||||
if (next >= maxScroll) {
|
||||
next = Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
const current = getScrollTop();
|
||||
const next = Math.min(
|
||||
Math.max(0, current + delta),
|
||||
Math.max(0, scrollHeight - innerHeight),
|
||||
);
|
||||
setPendingScrollTop(next);
|
||||
setScrollTop(next);
|
||||
},
|
||||
[getScrollTop, setPendingScrollTop],
|
||||
[sizeRef, getScrollTop, setPendingScrollTop],
|
||||
);
|
||||
|
||||
const { scrollbarColor, flashScrollbar, scrollByWithAnimation } =
|
||||
@@ -140,11 +107,10 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
||||
const { scrollHeight, innerHeight } = sizeRef.current;
|
||||
const scrollTop = getScrollTop();
|
||||
const maxScroll = Math.max(0, scrollHeight - innerHeight);
|
||||
const actualScrollTop = Math.min(scrollTop, maxScroll);
|
||||
|
||||
// Only capture scroll-up events if there's room;
|
||||
// otherwise allow events to bubble.
|
||||
if (actualScrollTop > 0) {
|
||||
if (scrollTop > 0) {
|
||||
if (keyMatchers[Command.PAGE_UP](key)) {
|
||||
scrollByWithAnimation(-innerHeight);
|
||||
return true;
|
||||
@@ -157,7 +123,7 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
||||
|
||||
// Only capture scroll-down events if there's room;
|
||||
// otherwise allow events to bubble.
|
||||
if (actualScrollTop < maxScroll) {
|
||||
if (scrollTop < maxScroll) {
|
||||
if (keyMatchers[Command.PAGE_DOWN](key)) {
|
||||
scrollByWithAnimation(innerHeight);
|
||||
return true;
|
||||
@@ -174,21 +140,21 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
||||
{ isActive: hasFocus },
|
||||
);
|
||||
|
||||
const getScrollState = useCallback(() => {
|
||||
const maxScroll = Math.max(0, size.scrollHeight - size.innerHeight);
|
||||
return {
|
||||
scrollTop: Math.min(getScrollTop(), maxScroll),
|
||||
const getScrollState = useCallback(
|
||||
() => ({
|
||||
scrollTop: getScrollTop(),
|
||||
scrollHeight: size.scrollHeight,
|
||||
innerHeight: size.innerHeight,
|
||||
};
|
||||
}, [getScrollTop, size.scrollHeight, size.innerHeight]);
|
||||
}),
|
||||
[getScrollTop, size.scrollHeight, size.innerHeight],
|
||||
);
|
||||
|
||||
const hasFocusCallback = useCallback(() => hasFocus, [hasFocus]);
|
||||
|
||||
const scrollableEntry = useMemo(
|
||||
() => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
ref: viewportRef as React.RefObject<DOMElement>,
|
||||
ref: ref as React.RefObject<DOMElement>,
|
||||
getScrollState,
|
||||
scrollBy: scrollByWithAnimation,
|
||||
hasFocus: hasFocusCallback,
|
||||
@@ -201,7 +167,7 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={viewportRefCallback}
|
||||
ref={ref}
|
||||
maxHeight={maxHeight}
|
||||
width={width ?? maxWidth}
|
||||
height={height}
|
||||
@@ -217,12 +183,7 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
||||
based on the children's content. It also adds a right padding to
|
||||
make room for the scrollbar.
|
||||
*/}
|
||||
<Box
|
||||
ref={contentRefCallback}
|
||||
flexShrink={0}
|
||||
paddingRight={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
<Box flexShrink={0} paddingRight={1} flexDirection="column">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -479,277 +479,4 @@ describe('ScrollableList Demo Behavior', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('regression: remove last item and add 2 items when scrolled to bottom', async () => {
|
||||
let listRef: ScrollableListRef<Item> | null = null;
|
||||
let setItemsFunc: React.Dispatch<React.SetStateAction<Item[]>> | null =
|
||||
null;
|
||||
|
||||
const TestComp = () => {
|
||||
const [items, setItems] = useState<Item[]>(
|
||||
Array.from({ length: 10 }, (_, i) => ({
|
||||
id: String(i),
|
||||
title: `Item ${i}`,
|
||||
})),
|
||||
);
|
||||
useEffect(() => {
|
||||
setItemsFunc = setItems;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MouseProvider mouseEventsEnabled={false}>
|
||||
<KeypressProvider>
|
||||
<ScrollProvider>
|
||||
<Box flexDirection="column" width={80} height={5}>
|
||||
<ScrollableList
|
||||
ref={(ref) => {
|
||||
listRef = ref;
|
||||
}}
|
||||
data={items}
|
||||
renderItem={({ item }) => <Text>{item.title}</Text>}
|
||||
estimatedItemHeight={() => 1}
|
||||
keyExtractor={(item) => item.id}
|
||||
hasFocus={true}
|
||||
initialScrollIndex={Number.MAX_SAFE_INTEGER}
|
||||
/>
|
||||
</Box>
|
||||
</ScrollProvider>
|
||||
</KeypressProvider>
|
||||
</MouseProvider>
|
||||
);
|
||||
};
|
||||
|
||||
let result: ReturnType<typeof render>;
|
||||
await act(async () => {
|
||||
result = render(<TestComp />);
|
||||
});
|
||||
|
||||
await result!.waitUntilReady();
|
||||
|
||||
// Scrolled to bottom, max scroll = 10 - 5 = 5
|
||||
await waitFor(() => {
|
||||
expect(listRef?.getScrollState()?.scrollTop).toBe(5);
|
||||
});
|
||||
|
||||
// Remove last element and add 2 elements
|
||||
await act(async () => {
|
||||
setItemsFunc!((prev) => {
|
||||
const next = prev.slice(0, prev.length - 1);
|
||||
next.push({ id: '10', title: 'Item 10' });
|
||||
next.push({ id: '11', title: 'Item 11' });
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
await result!.waitUntilReady();
|
||||
|
||||
// Auto scrolls to new bottom: max scroll = 11 - 5 = 6
|
||||
await waitFor(() => {
|
||||
expect(listRef?.getScrollState()?.scrollTop).toBe(6);
|
||||
});
|
||||
|
||||
// Scroll up slightly
|
||||
await act(async () => {
|
||||
listRef?.scrollBy(-2);
|
||||
});
|
||||
await result!.waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(listRef?.getScrollState()?.scrollTop).toBe(4);
|
||||
});
|
||||
|
||||
// Scroll back to bottom
|
||||
await act(async () => {
|
||||
listRef?.scrollToEnd();
|
||||
});
|
||||
await result!.waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(listRef?.getScrollState()?.scrollTop).toBe(6);
|
||||
});
|
||||
|
||||
// Add two more elements
|
||||
await act(async () => {
|
||||
setItemsFunc!((prev) => [
|
||||
...prev,
|
||||
{ id: '12', title: 'Item 12' },
|
||||
{ id: '13', title: 'Item 13' },
|
||||
]);
|
||||
});
|
||||
|
||||
await result!.waitUntilReady();
|
||||
|
||||
// Auto scrolls to bottom: max scroll = 13 - 5 = 8
|
||||
await waitFor(() => {
|
||||
expect(listRef?.getScrollState()?.scrollTop).toBe(8);
|
||||
});
|
||||
|
||||
result!.unmount();
|
||||
});
|
||||
|
||||
it('regression: bottom-most element changes size but list does not update', async () => {
|
||||
let listRef: ScrollableListRef<Item> | null = null;
|
||||
let expandLastFunc: (() => void) | null = null;
|
||||
|
||||
const ItemWithState = ({
|
||||
item,
|
||||
isLast,
|
||||
}: {
|
||||
item: Item;
|
||||
isLast: boolean;
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
useEffect(() => {
|
||||
if (isLast) {
|
||||
expandLastFunc = () => setExpanded(true);
|
||||
}
|
||||
}, [isLast]);
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>{item.title}</Text>
|
||||
{expanded && <Text>Expanded content</Text>}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const TestComp = () => {
|
||||
// items array is stable
|
||||
const [items] = useState(() =>
|
||||
Array.from({ length: 5 }, (_, i) => ({
|
||||
id: String(i),
|
||||
title: `Item ${i}`,
|
||||
})),
|
||||
);
|
||||
|
||||
return (
|
||||
<MouseProvider mouseEventsEnabled={false}>
|
||||
<KeypressProvider>
|
||||
<ScrollProvider>
|
||||
<Box flexDirection="column" width={80} height={4}>
|
||||
<ScrollableList
|
||||
ref={(ref) => {
|
||||
listRef = ref;
|
||||
}}
|
||||
data={items}
|
||||
renderItem={({ item, index }) => (
|
||||
<ItemWithState item={item} isLast={index === 4} />
|
||||
)}
|
||||
estimatedItemHeight={() => 1}
|
||||
keyExtractor={(item) => item.id}
|
||||
hasFocus={true}
|
||||
initialScrollIndex={Number.MAX_SAFE_INTEGER}
|
||||
/>
|
||||
</Box>
|
||||
</ScrollProvider>
|
||||
</KeypressProvider>
|
||||
</MouseProvider>
|
||||
);
|
||||
};
|
||||
|
||||
let result: ReturnType<typeof render>;
|
||||
await act(async () => {
|
||||
result = render(<TestComp />);
|
||||
});
|
||||
|
||||
await result!.waitUntilReady();
|
||||
|
||||
// Initially, total height is 5. viewport is 4. scroll is 1.
|
||||
await waitFor(() => {
|
||||
expect(listRef?.getScrollState()?.scrollTop).toBe(1);
|
||||
});
|
||||
|
||||
// Expand the last item locally, without re-rendering the list!
|
||||
await act(async () => {
|
||||
expandLastFunc!();
|
||||
});
|
||||
|
||||
await result!.waitUntilReady();
|
||||
|
||||
// The total height becomes 6. It should remain scrolled to bottom, so scroll becomes 2.
|
||||
// This is expected to FAIL currently because VirtualizedList won't remeasure
|
||||
// unless data changes or container height changes.
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(listRef?.getScrollState()?.scrollTop).toBe(2);
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
|
||||
result!.unmount();
|
||||
});
|
||||
|
||||
it('regression: prepending items does not corrupt heights (total height correct)', async () => {
|
||||
let listRef: ScrollableListRef<Item> | null = null;
|
||||
let setItemsFunc: React.Dispatch<React.SetStateAction<Item[]>> | null =
|
||||
null;
|
||||
|
||||
const TestComp = () => {
|
||||
// Items 1 to 5. Item 1 is very tall.
|
||||
const [items, setItems] = useState<Item[]>(
|
||||
Array.from({ length: 5 }, (_, i) => ({
|
||||
id: String(i + 1),
|
||||
title: `Item ${i + 1}`,
|
||||
})),
|
||||
);
|
||||
useEffect(() => {
|
||||
setItemsFunc = setItems;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MouseProvider mouseEventsEnabled={false}>
|
||||
<KeypressProvider>
|
||||
<ScrollProvider>
|
||||
<Box flexDirection="column" width={80} height={10}>
|
||||
<ScrollableList
|
||||
ref={(ref) => {
|
||||
listRef = ref;
|
||||
}}
|
||||
data={items}
|
||||
renderItem={({ item }) => (
|
||||
<Box height={item.id === '1' ? 10 : 2}>
|
||||
<Text>{item.title}</Text>
|
||||
</Box>
|
||||
)}
|
||||
estimatedItemHeight={() => 2}
|
||||
keyExtractor={(item) => item.id}
|
||||
hasFocus={true}
|
||||
initialScrollIndex={Number.MAX_SAFE_INTEGER}
|
||||
/>
|
||||
</Box>
|
||||
</ScrollProvider>
|
||||
</KeypressProvider>
|
||||
</MouseProvider>
|
||||
);
|
||||
};
|
||||
|
||||
let result: ReturnType<typeof render>;
|
||||
await act(async () => {
|
||||
result = render(<TestComp />);
|
||||
});
|
||||
|
||||
await result!.waitUntilReady();
|
||||
|
||||
// Scroll is at bottom.
|
||||
// Heights: Item 1: 10, Item 2: 2, Item 3: 2, Item 4: 2, Item 5: 2.
|
||||
// Total height = 18. Container = 10. Max scroll = 8.
|
||||
await waitFor(() => {
|
||||
expect(listRef?.getScrollState()?.scrollTop).toBe(8);
|
||||
});
|
||||
|
||||
// Prepend an item!
|
||||
await act(async () => {
|
||||
setItemsFunc!((prev) => [{ id: '0', title: 'Item 0' }, ...prev]);
|
||||
});
|
||||
|
||||
await result!.waitUntilReady();
|
||||
|
||||
// Now items: 0(2), 1(10), 2(2), 3(2), 4(2), 5(2).
|
||||
// Total height = 20. Container = 10. Max scroll = 10.
|
||||
// Auto-scrolls to bottom because it was sticking!
|
||||
await waitFor(() => {
|
||||
expect(listRef?.getScrollState()?.scrollTop).toBe(10);
|
||||
});
|
||||
|
||||
result!.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useImperativeHandle,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import type React from 'react';
|
||||
import {
|
||||
@@ -105,7 +105,7 @@ function ScrollableList<T>(
|
||||
smoothScrollState.current.active = false;
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => stopSmoothScroll, [stopSmoothScroll]);
|
||||
useEffect(() => stopSmoothScroll, [stopSmoothScroll]);
|
||||
|
||||
const smoothScrollTo = useCallback(
|
||||
(
|
||||
@@ -120,19 +120,15 @@ function ScrollableList<T>(
|
||||
innerHeight: 0,
|
||||
};
|
||||
const {
|
||||
scrollTop: rawStartScrollTop,
|
||||
scrollTop: startScrollTop,
|
||||
scrollHeight,
|
||||
innerHeight,
|
||||
} = scrollState;
|
||||
|
||||
const maxScrollTop = Math.max(0, scrollHeight - innerHeight);
|
||||
const startScrollTop = Math.min(rawStartScrollTop, maxScrollTop);
|
||||
|
||||
let effectiveTarget = targetScrollTop;
|
||||
if (
|
||||
targetScrollTop === SCROLL_TO_ITEM_END ||
|
||||
targetScrollTop >= maxScrollTop
|
||||
) {
|
||||
if (targetScrollTop === SCROLL_TO_ITEM_END) {
|
||||
effectiveTarget = maxScrollTop;
|
||||
}
|
||||
|
||||
@@ -142,11 +138,8 @@ function ScrollableList<T>(
|
||||
);
|
||||
|
||||
if (duration === 0) {
|
||||
if (
|
||||
targetScrollTop === SCROLL_TO_ITEM_END ||
|
||||
targetScrollTop >= maxScrollTop
|
||||
) {
|
||||
virtualizedListRef.current?.scrollTo(Number.MAX_SAFE_INTEGER);
|
||||
if (targetScrollTop === SCROLL_TO_ITEM_END) {
|
||||
virtualizedListRef.current?.scrollTo(SCROLL_TO_ITEM_END);
|
||||
} else {
|
||||
virtualizedListRef.current?.scrollTo(Math.round(clampedTarget));
|
||||
}
|
||||
@@ -175,11 +168,8 @@ function ScrollableList<T>(
|
||||
ease;
|
||||
|
||||
if (progress >= 1) {
|
||||
if (
|
||||
targetScrollTop === SCROLL_TO_ITEM_END ||
|
||||
targetScrollTop >= maxScrollTop
|
||||
) {
|
||||
virtualizedListRef.current?.scrollTo(Number.MAX_SAFE_INTEGER);
|
||||
if (targetScrollTop === SCROLL_TO_ITEM_END) {
|
||||
virtualizedListRef.current?.scrollTo(SCROLL_TO_ITEM_END);
|
||||
} else {
|
||||
virtualizedListRef.current?.scrollTo(Math.round(current));
|
||||
}
|
||||
@@ -210,13 +200,9 @@ function ScrollableList<T>(
|
||||
) {
|
||||
const direction = keyMatchers[Command.PAGE_UP](key) ? -1 : 1;
|
||||
const scrollState = getScrollState();
|
||||
const maxScroll = Math.max(
|
||||
0,
|
||||
scrollState.scrollHeight - scrollState.innerHeight,
|
||||
);
|
||||
const current = smoothScrollState.current.active
|
||||
? smoothScrollState.current.to
|
||||
: Math.min(scrollState.scrollTop, maxScroll);
|
||||
: scrollState.scrollTop;
|
||||
const innerHeight = scrollState.innerHeight;
|
||||
smoothScrollTo(current + direction * innerHeight);
|
||||
return true;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { waitFor } from '../../../test-utils/async.js';
|
||||
import { VirtualizedList, type VirtualizedListRef } from './VirtualizedList.js';
|
||||
import { Text, Box } from 'ink';
|
||||
import {
|
||||
@@ -276,11 +275,9 @@ describe('<VirtualizedList />', () => {
|
||||
await waitUntilReady();
|
||||
|
||||
// Now Item 0 is 1px, so Items 1-9 should also be visible to fill 10px
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Item 0');
|
||||
expect(lastFrame()).toContain('Item 1');
|
||||
expect(lastFrame()).toContain('Item 9');
|
||||
});
|
||||
expect(lastFrame()).toContain('Item 0');
|
||||
expect(lastFrame()).toContain('Item 1');
|
||||
expect(lastFrame()).toContain('Item 9');
|
||||
unmount();
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useLayoutEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
@@ -18,7 +19,7 @@ import { theme } from '../../semantic-colors.js';
|
||||
import { useBatchedScroll } from '../../hooks/useBatchedScroll.js';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
|
||||
import { type DOMElement, Box, ResizeObserver } from 'ink';
|
||||
import { type DOMElement, measureElement, Box } from 'ink';
|
||||
|
||||
export const SCROLL_TO_ITEM_END = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
@@ -80,7 +81,7 @@ function VirtualizedList<T>(
|
||||
} = props;
|
||||
const { copyModeEnabled } = useUIState();
|
||||
const dataRef = useRef(data);
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
dataRef.current = data;
|
||||
}, [data]);
|
||||
|
||||
@@ -107,7 +108,6 @@ function VirtualizedList<T>(
|
||||
|
||||
return { index: 0, offset: 0 };
|
||||
});
|
||||
|
||||
const [isStickingToBottom, setIsStickingToBottom] = useState(() => {
|
||||
const scrollToEnd =
|
||||
initialScrollIndex === SCROLL_TO_ITEM_END ||
|
||||
@@ -116,75 +116,73 @@ function VirtualizedList<T>(
|
||||
initialScrollOffsetInIndex === SCROLL_TO_ITEM_END);
|
||||
return scrollToEnd;
|
||||
});
|
||||
|
||||
const containerRef = useRef<DOMElement | null>(null);
|
||||
const containerRef = useRef<DOMElement>(null);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
const itemRefs = useRef<Array<DOMElement | null>>([]);
|
||||
const [heights, setHeights] = useState<Record<string, number>>({});
|
||||
const [heights, setHeights] = useState<number[]>([]);
|
||||
const isInitialScrollSet = useRef(false);
|
||||
|
||||
const containerObserverRef = useRef<ResizeObserver | null>(null);
|
||||
const nodeToKeyRef = useRef(new WeakMap<DOMElement, string>());
|
||||
|
||||
const containerRefCallback = useCallback((node: DOMElement | null) => {
|
||||
containerObserverRef.current?.disconnect();
|
||||
containerRef.current = node;
|
||||
if (node) {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
setContainerHeight(Math.round(entry.contentRect.height));
|
||||
}
|
||||
});
|
||||
observer.observe(node);
|
||||
containerObserverRef.current = observer;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const itemsObserver = useMemo(
|
||||
() =>
|
||||
new ResizeObserver((entries) => {
|
||||
setHeights((prev) => {
|
||||
let next: Record<string, number> | null = null;
|
||||
for (const entry of entries) {
|
||||
const key = nodeToKeyRef.current.get(entry.target);
|
||||
if (key !== undefined) {
|
||||
const height = Math.round(entry.contentRect.height);
|
||||
if (prev[key] !== height) {
|
||||
if (!next) {
|
||||
next = { ...prev };
|
||||
}
|
||||
next[key] = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
return next ?? prev;
|
||||
});
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useLayoutEffect(
|
||||
() => () => {
|
||||
containerObserverRef.current?.disconnect();
|
||||
itemsObserver.disconnect();
|
||||
},
|
||||
[itemsObserver],
|
||||
);
|
||||
|
||||
const { totalHeight, offsets } = useMemo(() => {
|
||||
const offsets: number[] = [0];
|
||||
let totalHeight = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const key = keyExtractor(data[i], i);
|
||||
const height = heights[key] ?? estimatedItemHeight(i);
|
||||
const height = heights[i] ?? estimatedItemHeight(i);
|
||||
totalHeight += height;
|
||||
offsets.push(totalHeight);
|
||||
}
|
||||
return { totalHeight, offsets };
|
||||
}, [heights, data, estimatedItemHeight, keyExtractor]);
|
||||
}, [heights, data, estimatedItemHeight]);
|
||||
|
||||
const scrollableContainerHeight = containerHeight;
|
||||
useEffect(() => {
|
||||
setHeights((prevHeights) => {
|
||||
if (data.length === prevHeights.length) {
|
||||
return prevHeights;
|
||||
}
|
||||
|
||||
const newHeights = [...prevHeights];
|
||||
if (data.length < prevHeights.length) {
|
||||
newHeights.length = data.length;
|
||||
} else {
|
||||
for (let i = prevHeights.length; i < data.length; i++) {
|
||||
newHeights[i] = estimatedItemHeight(i);
|
||||
}
|
||||
}
|
||||
return newHeights;
|
||||
});
|
||||
}, [data, estimatedItemHeight]);
|
||||
|
||||
// This layout effect needs to run on every render to correctly measure the
|
||||
// container and ensure we recompute the layout if it has changed.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useLayoutEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const height = Math.round(measureElement(containerRef.current).height);
|
||||
if (containerHeight !== height) {
|
||||
setContainerHeight(height);
|
||||
}
|
||||
}
|
||||
|
||||
let newHeights: number[] | null = null;
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const itemRef = itemRefs.current[i];
|
||||
if (itemRef) {
|
||||
const height = Math.round(measureElement(itemRef).height);
|
||||
if (height !== heights[i]) {
|
||||
if (!newHeights) {
|
||||
newHeights = [...heights];
|
||||
}
|
||||
newHeights[i] = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newHeights) {
|
||||
setHeights(newHeights);
|
||||
}
|
||||
});
|
||||
|
||||
const scrollableContainerHeight = containerRef.current
|
||||
? Math.round(measureElement(containerRef.current).height)
|
||||
: containerHeight;
|
||||
|
||||
const getAnchorForScrollTop = useCallback(
|
||||
(
|
||||
@@ -201,36 +199,23 @@ function VirtualizedList<T>(
|
||||
[],
|
||||
);
|
||||
|
||||
const actualScrollTop = useMemo(() => {
|
||||
const scrollTop = useMemo(() => {
|
||||
const offset = offsets[scrollAnchor.index];
|
||||
if (typeof offset !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (scrollAnchor.offset === SCROLL_TO_ITEM_END) {
|
||||
const item = data[scrollAnchor.index];
|
||||
const key = item ? keyExtractor(item, scrollAnchor.index) : '';
|
||||
const itemHeight = heights[key] ?? 0;
|
||||
const itemHeight = heights[scrollAnchor.index] ?? 0;
|
||||
return offset + itemHeight - scrollableContainerHeight;
|
||||
}
|
||||
|
||||
return offset + scrollAnchor.offset;
|
||||
}, [
|
||||
scrollAnchor,
|
||||
offsets,
|
||||
heights,
|
||||
scrollableContainerHeight,
|
||||
data,
|
||||
keyExtractor,
|
||||
]);
|
||||
|
||||
const scrollTop = isStickingToBottom
|
||||
? Number.MAX_SAFE_INTEGER
|
||||
: actualScrollTop;
|
||||
}, [scrollAnchor, offsets, heights, scrollableContainerHeight]);
|
||||
|
||||
const prevDataLength = useRef(data.length);
|
||||
const prevTotalHeight = useRef(totalHeight);
|
||||
const prevScrollTop = useRef(actualScrollTop);
|
||||
const prevScrollTop = useRef(scrollTop);
|
||||
const prevContainerHeight = useRef(scrollableContainerHeight);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -241,7 +226,9 @@ function VirtualizedList<T>(
|
||||
prevTotalHeight.current - prevContainerHeight.current - 1;
|
||||
const wasAtBottom = contentPreviouslyFit || wasScrolledToBottomPixels;
|
||||
|
||||
if (wasAtBottom && actualScrollTop >= prevScrollTop.current) {
|
||||
// If the user was at the bottom, they are now sticking. This handles
|
||||
// manually scrolling back to the bottom.
|
||||
if (wasAtBottom && scrollTop >= prevScrollTop.current) {
|
||||
setIsStickingToBottom(true);
|
||||
}
|
||||
|
||||
@@ -249,6 +236,9 @@ function VirtualizedList<T>(
|
||||
const containerChanged =
|
||||
prevContainerHeight.current !== scrollableContainerHeight;
|
||||
|
||||
// We scroll to the end if:
|
||||
// 1. The list grew AND we were already at the bottom (or sticking).
|
||||
// 2. We are sticking to the bottom AND the container size changed.
|
||||
if (
|
||||
(listGrew && (isStickingToBottom || wasAtBottom)) ||
|
||||
(isStickingToBottom && containerChanged)
|
||||
@@ -257,28 +247,34 @@ function VirtualizedList<T>(
|
||||
index: data.length > 0 ? data.length - 1 : 0,
|
||||
offset: SCROLL_TO_ITEM_END,
|
||||
});
|
||||
// If we are scrolling to the bottom, we are by definition sticking.
|
||||
if (!isStickingToBottom) {
|
||||
setIsStickingToBottom(true);
|
||||
}
|
||||
} else if (
|
||||
}
|
||||
// Scenario 2: The list has changed (shrunk) in a way that our
|
||||
// current scroll position or anchor is invalid. We should adjust to the bottom.
|
||||
else if (
|
||||
(scrollAnchor.index >= data.length ||
|
||||
actualScrollTop > totalHeight - scrollableContainerHeight) &&
|
||||
scrollTop > totalHeight - scrollableContainerHeight) &&
|
||||
data.length > 0
|
||||
) {
|
||||
const newScrollTop = Math.max(0, totalHeight - scrollableContainerHeight);
|
||||
setScrollAnchor(getAnchorForScrollTop(newScrollTop, offsets));
|
||||
} else if (data.length === 0) {
|
||||
// List is now empty, reset scroll to top.
|
||||
setScrollAnchor({ index: 0, offset: 0 });
|
||||
}
|
||||
|
||||
// Update refs for the next render cycle.
|
||||
prevDataLength.current = data.length;
|
||||
prevTotalHeight.current = totalHeight;
|
||||
prevScrollTop.current = actualScrollTop;
|
||||
prevScrollTop.current = scrollTop;
|
||||
prevContainerHeight.current = scrollableContainerHeight;
|
||||
}, [
|
||||
data.length,
|
||||
totalHeight,
|
||||
actualScrollTop,
|
||||
scrollTop,
|
||||
scrollableContainerHeight,
|
||||
scrollAnchor.index,
|
||||
getAnchorForScrollTop,
|
||||
@@ -338,10 +334,10 @@ function VirtualizedList<T>(
|
||||
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
findLastIndex(offsets, (offset) => offset <= actualScrollTop) - 1,
|
||||
findLastIndex(offsets, (offset) => offset <= scrollTop) - 1,
|
||||
);
|
||||
const endIndexOffset = offsets.findIndex(
|
||||
(offset) => offset > actualScrollTop + scrollableContainerHeight,
|
||||
(offset) => offset > scrollTop + scrollableContainerHeight,
|
||||
);
|
||||
const endIndex =
|
||||
endIndexOffset === -1
|
||||
@@ -352,32 +348,6 @@ function VirtualizedList<T>(
|
||||
const bottomSpacerHeight =
|
||||
totalHeight - (offsets[endIndex + 1] ?? totalHeight);
|
||||
|
||||
// Maintain a stable set of observed nodes using useLayoutEffect
|
||||
const observedNodes = useRef<Set<DOMElement>>(new Set());
|
||||
useLayoutEffect(() => {
|
||||
const currentNodes = new Set<DOMElement>();
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const node = itemRefs.current[i];
|
||||
const item = data[i];
|
||||
if (node && item) {
|
||||
currentNodes.add(node);
|
||||
const key = keyExtractor(item, i);
|
||||
// Always update the key mapping because React can reuse nodes at different indices/keys
|
||||
nodeToKeyRef.current.set(node, key);
|
||||
if (!observedNodes.current.has(node)) {
|
||||
itemsObserver.observe(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const node of observedNodes.current) {
|
||||
if (!currentNodes.has(node)) {
|
||||
itemsObserver.unobserve(node);
|
||||
nodeToKeyRef.current.delete(node);
|
||||
}
|
||||
}
|
||||
observedNodes.current = currentNodes;
|
||||
});
|
||||
|
||||
const renderedItems = [];
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const item = data[i];
|
||||
@@ -386,8 +356,6 @@ function VirtualizedList<T>(
|
||||
<Box
|
||||
key={keyExtractor(item, i)}
|
||||
width="100%"
|
||||
flexDirection="column"
|
||||
flexShrink={0}
|
||||
ref={(el) => {
|
||||
itemRefs.current[i] = el;
|
||||
}}
|
||||
@@ -408,39 +376,27 @@ function VirtualizedList<T>(
|
||||
setIsStickingToBottom(false);
|
||||
}
|
||||
const currentScrollTop = getScrollTop();
|
||||
const maxScroll = Math.max(0, totalHeight - scrollableContainerHeight);
|
||||
const actualCurrent = Math.min(currentScrollTop, maxScroll);
|
||||
let newScrollTop = Math.max(0, actualCurrent + delta);
|
||||
if (newScrollTop >= maxScroll) {
|
||||
setIsStickingToBottom(true);
|
||||
newScrollTop = Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
setPendingScrollTop(newScrollTop);
|
||||
setScrollAnchor(
|
||||
getAnchorForScrollTop(Math.min(newScrollTop, maxScroll), offsets),
|
||||
const newScrollTop = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
totalHeight - scrollableContainerHeight,
|
||||
currentScrollTop + delta,
|
||||
),
|
||||
);
|
||||
setPendingScrollTop(newScrollTop);
|
||||
setScrollAnchor(getAnchorForScrollTop(newScrollTop, offsets));
|
||||
},
|
||||
scrollTo: (offset: number) => {
|
||||
const maxScroll = Math.max(0, totalHeight - scrollableContainerHeight);
|
||||
if (offset >= maxScroll || offset === SCROLL_TO_ITEM_END) {
|
||||
setIsStickingToBottom(true);
|
||||
setPendingScrollTop(Number.MAX_SAFE_INTEGER);
|
||||
if (data.length > 0) {
|
||||
setScrollAnchor({
|
||||
index: data.length - 1,
|
||||
offset: SCROLL_TO_ITEM_END,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setIsStickingToBottom(false);
|
||||
const newScrollTop = Math.max(0, offset);
|
||||
setPendingScrollTop(newScrollTop);
|
||||
setScrollAnchor(getAnchorForScrollTop(newScrollTop, offsets));
|
||||
}
|
||||
setIsStickingToBottom(false);
|
||||
const newScrollTop = Math.max(
|
||||
0,
|
||||
Math.min(totalHeight - scrollableContainerHeight, offset),
|
||||
);
|
||||
setPendingScrollTop(newScrollTop);
|
||||
setScrollAnchor(getAnchorForScrollTop(newScrollTop, offsets));
|
||||
},
|
||||
scrollToEnd: () => {
|
||||
setIsStickingToBottom(true);
|
||||
setPendingScrollTop(Number.MAX_SAFE_INTEGER);
|
||||
if (data.length > 0) {
|
||||
setScrollAnchor({
|
||||
index: data.length - 1,
|
||||
@@ -460,14 +416,10 @@ function VirtualizedList<T>(
|
||||
setIsStickingToBottom(false);
|
||||
const offset = offsets[index];
|
||||
if (offset !== undefined) {
|
||||
const maxScroll = Math.max(
|
||||
0,
|
||||
totalHeight - scrollableContainerHeight,
|
||||
);
|
||||
const newScrollTop = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
maxScroll,
|
||||
totalHeight - scrollableContainerHeight,
|
||||
offset - viewPosition * scrollableContainerHeight + viewOffset,
|
||||
),
|
||||
);
|
||||
@@ -489,14 +441,10 @@ function VirtualizedList<T>(
|
||||
if (index !== -1) {
|
||||
const offset = offsets[index];
|
||||
if (offset !== undefined) {
|
||||
const maxScroll = Math.max(
|
||||
0,
|
||||
totalHeight - scrollableContainerHeight,
|
||||
);
|
||||
const newScrollTop = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
maxScroll,
|
||||
totalHeight - scrollableContainerHeight,
|
||||
offset - viewPosition * scrollableContainerHeight + viewOffset,
|
||||
),
|
||||
);
|
||||
@@ -506,14 +454,11 @@ function VirtualizedList<T>(
|
||||
}
|
||||
},
|
||||
getScrollIndex: () => scrollAnchor.index,
|
||||
getScrollState: () => {
|
||||
const maxScroll = Math.max(0, totalHeight - containerHeight);
|
||||
return {
|
||||
scrollTop: Math.min(getScrollTop(), maxScroll),
|
||||
scrollHeight: totalHeight,
|
||||
innerHeight: containerHeight,
|
||||
};
|
||||
},
|
||||
getScrollState: () => ({
|
||||
scrollTop: getScrollTop(),
|
||||
scrollHeight: totalHeight,
|
||||
innerHeight: containerHeight,
|
||||
}),
|
||||
}),
|
||||
[
|
||||
offsets,
|
||||
@@ -530,7 +475,7 @@ function VirtualizedList<T>(
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={containerRefCallback}
|
||||
ref={containerRef}
|
||||
overflowY={copyModeEnabled ? 'hidden' : 'scroll'}
|
||||
overflowX="hidden"
|
||||
scrollTop={copyModeEnabled ? 0 : scrollTop}
|
||||
@@ -544,7 +489,7 @@ function VirtualizedList<T>(
|
||||
flexShrink={0}
|
||||
width="100%"
|
||||
flexDirection="column"
|
||||
marginTop={copyModeEnabled ? -actualScrollTop : 0}
|
||||
marginTop={copyModeEnabled ? -scrollTop : 0}
|
||||
>
|
||||
<Box height={topSpacerHeight} flexShrink={0} />
|
||||
{renderedItems}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<MaxSizedBox /> > accounts for additionalHiddenLinesCount 1`] = `
|
||||
"... first 7 lines hidden (Ctrl+O to show) ...
|
||||
"... first 7 lines hidden ...
|
||||
Line 3
|
||||
"
|
||||
`;
|
||||
@@ -16,12 +16,12 @@ Line 6
|
||||
Line 7
|
||||
Line 8
|
||||
Line 9
|
||||
... last 21 lines hidden (Ctrl+O to show) ...
|
||||
... last 21 lines hidden ...
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > clips a long single text child from the top 1`] = `
|
||||
"... first 21 lines hidden (Ctrl+O to show) ...
|
||||
"... first 21 lines hidden ...
|
||||
Line 22
|
||||
Line 23
|
||||
Line 24
|
||||
@@ -39,7 +39,7 @@ exports[`<MaxSizedBox /> > does not leak content after hidden indicator with bot
|
||||
|
||||
- Step 1: Do something important
|
||||
- Step 2: Do something important
|
||||
... last 18 lines hidden (Ctrl+O to show) ...
|
||||
... last 18 lines hidden ...
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -58,12 +58,12 @@ Line 3 direct child
|
||||
|
||||
exports[`<MaxSizedBox /> > hides lines at the end when content exceeds maxHeight and overflowDirection is bottom 1`] = `
|
||||
"Line 1
|
||||
... last 2 lines hidden (Ctrl+O to show) ...
|
||||
... last 2 lines hidden ...
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > hides lines when content exceeds maxHeight 1`] = `
|
||||
"... first 2 lines hidden (Ctrl+O to show) ...
|
||||
"... first 2 lines hidden ...
|
||||
Line 3
|
||||
"
|
||||
`;
|
||||
@@ -74,13 +74,13 @@ exports[`<MaxSizedBox /> > renders children without truncation when they fit 1`]
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > shows plural "lines" when more than one line is hidden 1`] = `
|
||||
"... first 2 lines hidden (Ctrl+O to show) ...
|
||||
"... first 2 lines hidden ...
|
||||
Line 3
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > shows singular "line" when exactly one line is hidden 1`] = `
|
||||
"... first 1 line hidden (Ctrl+O to show) ...
|
||||
"... first 1 line hidden ...
|
||||
Line 1
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -24,7 +24,6 @@ import type {
|
||||
ApprovalMode,
|
||||
UserTierId,
|
||||
IdeInfo,
|
||||
AuthType,
|
||||
FallbackIntent,
|
||||
ValidationIntent,
|
||||
AgentDefinition,
|
||||
@@ -43,7 +42,6 @@ export interface ProQuotaDialogRequest {
|
||||
message: string;
|
||||
isTerminalQuotaError: boolean;
|
||||
isModelNotFoundError?: boolean;
|
||||
authType?: AuthType;
|
||||
resolve: (intent: FallbackIntent) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import {
|
||||
useAlternateBuffer,
|
||||
isAlternateBufferEnabled,
|
||||
} from './useAlternateBuffer.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../contexts/ConfigContext.js', () => ({
|
||||
useConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUseConfig = vi.mocked(
|
||||
await import('../contexts/ConfigContext.js').then((m) => m.useConfig),
|
||||
);
|
||||
|
||||
describe('useAlternateBuffer', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return false when config.getUseAlternateBuffer returns false', () => {
|
||||
mockUseConfig.mockReturnValue({
|
||||
getUseAlternateBuffer: () => false,
|
||||
} as unknown as ReturnType<typeof mockUseConfig>);
|
||||
|
||||
const { result } = renderHook(() => useAlternateBuffer());
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when config.getUseAlternateBuffer returns true', () => {
|
||||
mockUseConfig.mockReturnValue({
|
||||
getUseAlternateBuffer: () => true,
|
||||
} as unknown as ReturnType<typeof mockUseConfig>);
|
||||
|
||||
const { result } = renderHook(() => useAlternateBuffer());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return the immutable config value, not react to settings changes', () => {
|
||||
const mockConfig = {
|
||||
getUseAlternateBuffer: () => true,
|
||||
} as unknown as ReturnType<typeof mockUseConfig>;
|
||||
|
||||
mockUseConfig.mockReturnValue(mockConfig);
|
||||
|
||||
const { result, rerender } = renderHook(() => useAlternateBuffer());
|
||||
|
||||
// Value should remain true even after rerender
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAlternateBufferEnabled', () => {
|
||||
it('should return true when config.getUseAlternateBuffer returns true', () => {
|
||||
const config = {
|
||||
getUseAlternateBuffer: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
expect(isAlternateBufferEnabled(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when config.getUseAlternateBuffer returns false', () => {
|
||||
const config = {
|
||||
getUseAlternateBuffer: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
expect(isAlternateBufferEnabled(config)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -4,14 +4,13 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
|
||||
export const isAlternateBufferEnabled = (config: Config): boolean =>
|
||||
config.getUseAlternateBuffer();
|
||||
export const isAlternateBufferEnabled = (settings: LoadedSettings): boolean =>
|
||||
settings.merged.ui.useAlternateBuffer === true;
|
||||
|
||||
// This is read from Config so that the UI reads the same value per application session
|
||||
export const useAlternateBuffer = (): boolean => {
|
||||
const config = useConfig();
|
||||
return isAlternateBufferEnabled(config);
|
||||
const settings = useSettings();
|
||||
return isAlternateBufferEnabled(settings);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,6 @@ import type {
|
||||
Config,
|
||||
EditorType,
|
||||
AnyToolInvocation,
|
||||
SpanMetadata,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
CoreToolCallStatus,
|
||||
@@ -40,7 +39,6 @@ import {
|
||||
coreEvents,
|
||||
CoreEvent,
|
||||
MCPDiscoveryState,
|
||||
GeminiCliOperation,
|
||||
getPlanModeExitMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Part, PartListUnion } from '@google/genai';
|
||||
@@ -103,19 +101,6 @@ const MockValidationRequiredError = vi.hoisted(
|
||||
},
|
||||
);
|
||||
|
||||
const mockRunInDevTraceSpan = vi.hoisted(() =>
|
||||
vi.fn(async (opts, fn) => {
|
||||
const metadata: SpanMetadata = {
|
||||
name: opts.operation,
|
||||
attributes: opts.attributes || {},
|
||||
};
|
||||
return await fn({
|
||||
metadata,
|
||||
endSpan: vi.fn(),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actualCoreModule = (await importOriginal()) as any;
|
||||
return {
|
||||
@@ -128,7 +113,6 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
tokenLimit: vi.fn().mockReturnValue(100), // Mock tokenLimit
|
||||
recordToolCallInteractions: vi.fn().mockResolvedValue(undefined),
|
||||
getCodeAssistServer: vi.fn().mockReturnValue(undefined),
|
||||
runInDevTraceSpan: mockRunInDevTraceSpan,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -810,23 +794,6 @@ describe('useGeminiStream', () => {
|
||||
item.text.includes('Got it. Focusing on tests only.'),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
expect(mockRunInDevTraceSpan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
operation: GeminiCliOperation.SystemPrompt,
|
||||
}),
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
const spanArgs = mockRunInDevTraceSpan.mock.calls[0];
|
||||
const fn = spanArgs[1];
|
||||
const metadata = { attributes: {} };
|
||||
await act(async () => {
|
||||
await fn({ metadata, endSpan: vi.fn() });
|
||||
});
|
||||
expect(metadata).toMatchObject({
|
||||
input: sentParts,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle all tool calls being cancelled', async () => {
|
||||
@@ -2485,11 +2452,6 @@ describe('useGeminiStream', () => {
|
||||
// This is the core fix validation: Rationale comes before tools are even scheduled (awaited)
|
||||
expect(rationaleIndex).toBeLessThan(scheduleIndex);
|
||||
expect(rationaleIndex).toBeLessThan(toolGroupIndex);
|
||||
|
||||
// Ensure all state updates from recursive submitQuery are settled
|
||||
await waitFor(() => {
|
||||
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
||||
});
|
||||
});
|
||||
|
||||
it('should process @include commands, adding user turn after processing to prevent race conditions', async () => {
|
||||
@@ -3592,31 +3554,4 @@ describe('useGeminiStream', () => {
|
||||
expect(result.current.pendingHistoryItems.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should trace UserPrompt telemetry on submitQuery', async () => {
|
||||
const { result } = renderTestHook();
|
||||
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield { type: ServerGeminiEventType.Content, value: 'Response' };
|
||||
})(),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('telemetry test query');
|
||||
});
|
||||
|
||||
const userPromptCall = mockRunInDevTraceSpan.mock.calls.find(
|
||||
(call) =>
|
||||
call[0].operation === GeminiCliOperation.UserPrompt ||
|
||||
call[0].operation === 'UserPrompt',
|
||||
);
|
||||
expect(userPromptCall).toBeDefined();
|
||||
|
||||
const spanMetadata = {} as SpanMetadata;
|
||||
await act(async () => {
|
||||
await userPromptCall });
|
||||
});
|
||||
expect(spanMetadata.input).toBe('telemetry test query');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
CoreToolCallStatus,
|
||||
buildUserSteeringHintPrompt,
|
||||
generateSteeringAckMessage,
|
||||
GeminiCliOperation,
|
||||
getPlanModeExitMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
@@ -1263,11 +1262,7 @@ export const useGeminiStream = (
|
||||
prompt_id?: string,
|
||||
) =>
|
||||
runInDevTraceSpan(
|
||||
{
|
||||
operation: options?.isContinuation
|
||||
? GeminiCliOperation.SystemPrompt
|
||||
: GeminiCliOperation.UserPrompt,
|
||||
},
|
||||
{ name: 'submitQuery' },
|
||||
async ({ metadata: spanMetadata }) => {
|
||||
spanMetadata.input = query;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
type CodeAssistServer,
|
||||
UserTierId,
|
||||
getCodeAssistServer,
|
||||
debugLogger,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export interface PrivacyState {
|
||||
@@ -104,12 +103,7 @@ async function getRemoteDataCollectionOptIn(
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const resp = await server.getCodeAssistGlobalUserSetting();
|
||||
if (resp.freeTierDataCollectionOptin === undefined) {
|
||||
debugLogger.warn(
|
||||
'Warning: Code Assist API did not return freeTierDataCollectionOptin. Defaulting to true.',
|
||||
);
|
||||
}
|
||||
return resp.freeTierDataCollectionOptin ?? true;
|
||||
return resp.freeTierDataCollectionOptin;
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === 'object' && 'response' in error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
@@ -134,10 +128,5 @@ async function setRemoteDataCollectionOptIn(
|
||||
cloudaicompanionProject: server.projectId,
|
||||
freeTierDataCollectionOptin: optIn,
|
||||
});
|
||||
if (resp.freeTierDataCollectionOptin === undefined) {
|
||||
debugLogger.warn(
|
||||
`Warning: Code Assist API did not return freeTierDataCollectionOptin. Defaulting to ${optIn}.`,
|
||||
);
|
||||
}
|
||||
return resp.freeTierDataCollectionOptin ?? optIn;
|
||||
return resp.freeTierDataCollectionOptin;
|
||||
}
|
||||
|
||||
@@ -96,13 +96,9 @@ describe('useQuotaAndFallback', () => {
|
||||
});
|
||||
|
||||
describe('Fallback Handler Logic', () => {
|
||||
it('should show fallback dialog but omit switch to API key message if authType is not LOGIN_WITH_GOOGLE', async () => {
|
||||
// Override the default mock from beforeEach for this specific test
|
||||
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.USE_GEMINI,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
// Helper function to render the hook and extract the registered handler
|
||||
const getRegisteredHandler = (): FallbackModelHandler => {
|
||||
renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
@@ -111,24 +107,20 @@ describe('useQuotaAndFallback', () => {
|
||||
onShowAuthSelection: mockOnShowAuthSelection,
|
||||
}),
|
||||
);
|
||||
return setFallbackHandlerSpy.mock.calls[0][0] as FallbackModelHandler;
|
||||
};
|
||||
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
|
||||
const error = new TerminalQuotaError(
|
||||
'pro quota',
|
||||
mockGoogleApiError,
|
||||
1000 * 60 * 5,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
void handler('gemini-pro', 'gemini-flash', error);
|
||||
it('should return null and take no action if authType is not LOGIN_WITH_GOOGLE', async () => {
|
||||
// Override the default mock from beforeEach for this specific test
|
||||
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.USE_GEMINI,
|
||||
});
|
||||
|
||||
expect(result.current.proQuotaRequest).not.toBeNull();
|
||||
expect(result.current.proQuotaRequest?.message).not.toContain(
|
||||
'/auth to switch to API key.',
|
||||
);
|
||||
const handler = getRegisteredHandler();
|
||||
const result = await handler('gemini-pro', 'gemini-flash', new Error());
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('Interactive Fallback', () => {
|
||||
|
||||
@@ -55,7 +55,14 @@ export function useQuotaAndFallback({
|
||||
fallbackModel,
|
||||
error,
|
||||
): Promise<FallbackIntent | null> => {
|
||||
// Fallbacks are currently only handled for OAuth users.
|
||||
const contentGeneratorConfig = config.getContentGeneratorConfig();
|
||||
if (
|
||||
!contentGeneratorConfig ||
|
||||
contentGeneratorConfig.authType !== AuthType.LOGIN_WITH_GOOGLE
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let message: string;
|
||||
let isTerminalQuotaError = false;
|
||||
@@ -71,9 +78,7 @@ export function useQuotaAndFallback({
|
||||
error.retryDelayMs ? getResetTimeMessage(error.retryDelayMs) : null,
|
||||
`/stats model for usage details`,
|
||||
`/model to switch models.`,
|
||||
contentGeneratorConfig?.authType === AuthType.LOGIN_WITH_GOOGLE
|
||||
? `/auth to switch to API key.`
|
||||
: null,
|
||||
`/auth to switch to API key.`,
|
||||
].filter(Boolean);
|
||||
message = messageLines.join('\n');
|
||||
} else if (error instanceof ModelNotFoundError) {
|
||||
@@ -117,7 +122,6 @@ export function useQuotaAndFallback({
|
||||
message,
|
||||
isTerminalQuotaError,
|
||||
isModelNotFoundError,
|
||||
authType: contentGeneratorConfig?.authType,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -15,16 +15,14 @@ export function useTimedMessage<T>(durationMs: number) {
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const showMessage = useCallback(
|
||||
(msg: T | null) => {
|
||||
(msg: T) => {
|
||||
setMessage(msg);
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
if (msg !== null) {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setMessage(null);
|
||||
}, durationMs);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setMessage(null);
|
||||
}, durationMs);
|
||||
},
|
||||
[durationMs],
|
||||
);
|
||||
|
||||
@@ -6,9 +6,18 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { parseMarkdownToANSI } from './markdownParsingUtils.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { stripUnsafeCharacters } from './textUtils.js';
|
||||
|
||||
// Constants for Markdown parsing
|
||||
const BOLD_MARKER_LENGTH = 2; // For "**"
|
||||
const ITALIC_MARKER_LENGTH = 1; // For "*" or "_"
|
||||
const STRIKETHROUGH_MARKER_LENGTH = 2; // For "~~")
|
||||
const INLINE_CODE_MARKER_LENGTH = 1; // For "`"
|
||||
const UNDERLINE_TAG_START_LENGTH = 3; // For "<u>"
|
||||
const UNDERLINE_TAG_END_LENGTH = 4; // For "</u>"
|
||||
|
||||
interface RenderInlineProps {
|
||||
text: string;
|
||||
defaultColor?: string;
|
||||
@@ -19,9 +28,147 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({
|
||||
defaultColor,
|
||||
}) => {
|
||||
const text = stripUnsafeCharacters(rawText);
|
||||
const ansiText = parseMarkdownToANSI(text, defaultColor);
|
||||
const baseColor = defaultColor ?? theme.text.primary;
|
||||
// Early return for plain text without markdown or URLs
|
||||
if (!/[*_~`<[https?:]/.test(text)) {
|
||||
return <Text color={baseColor}>{text}</Text>;
|
||||
}
|
||||
|
||||
return <Text>{ansiText}</Text>;
|
||||
const nodes: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
const inlineRegex =
|
||||
/(\*\*.*?\*\*|\*.*?\*|_.*?_|~~.*?~~|\[.*?\]\(.*?\)|`+.+?`+|<u>.*?<\/u>|https?:\/\/\S+)/g;
|
||||
let match;
|
||||
|
||||
while ((match = inlineRegex.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
nodes.push(
|
||||
<Text key={`t-${lastIndex}`} color={baseColor}>
|
||||
{text.slice(lastIndex, match.index)}
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
|
||||
const fullMatch = match[0];
|
||||
let renderedNode: React.ReactNode = null;
|
||||
const key = `m-${match.index}`;
|
||||
|
||||
try {
|
||||
if (
|
||||
fullMatch.startsWith('**') &&
|
||||
fullMatch.endsWith('**') &&
|
||||
fullMatch.length > BOLD_MARKER_LENGTH * 2
|
||||
) {
|
||||
renderedNode = (
|
||||
<Text key={key} bold color={baseColor}>
|
||||
{fullMatch.slice(BOLD_MARKER_LENGTH, -BOLD_MARKER_LENGTH)}
|
||||
</Text>
|
||||
);
|
||||
} else if (
|
||||
fullMatch.length > ITALIC_MARKER_LENGTH * 2 &&
|
||||
((fullMatch.startsWith('*') && fullMatch.endsWith('*')) ||
|
||||
(fullMatch.startsWith('_') && fullMatch.endsWith('_'))) &&
|
||||
!/\w/.test(text.substring(match.index - 1, match.index)) &&
|
||||
!/\w/.test(
|
||||
text.substring(inlineRegex.lastIndex, inlineRegex.lastIndex + 1),
|
||||
) &&
|
||||
!/\S[./\\]/.test(text.substring(match.index - 2, match.index)) &&
|
||||
!/[./\\]\S/.test(
|
||||
text.substring(inlineRegex.lastIndex, inlineRegex.lastIndex + 2),
|
||||
)
|
||||
) {
|
||||
renderedNode = (
|
||||
<Text key={key} italic color={baseColor}>
|
||||
{fullMatch.slice(ITALIC_MARKER_LENGTH, -ITALIC_MARKER_LENGTH)}
|
||||
</Text>
|
||||
);
|
||||
} else if (
|
||||
fullMatch.startsWith('~~') &&
|
||||
fullMatch.endsWith('~~') &&
|
||||
fullMatch.length > STRIKETHROUGH_MARKER_LENGTH * 2
|
||||
) {
|
||||
renderedNode = (
|
||||
<Text key={key} strikethrough color={baseColor}>
|
||||
{fullMatch.slice(
|
||||
STRIKETHROUGH_MARKER_LENGTH,
|
||||
-STRIKETHROUGH_MARKER_LENGTH,
|
||||
)}
|
||||
</Text>
|
||||
);
|
||||
} else if (
|
||||
fullMatch.startsWith('`') &&
|
||||
fullMatch.endsWith('`') &&
|
||||
fullMatch.length > INLINE_CODE_MARKER_LENGTH
|
||||
) {
|
||||
const codeMatch = fullMatch.match(/^(`+)(.+?)\1$/s);
|
||||
if (codeMatch && codeMatch[2]) {
|
||||
renderedNode = (
|
||||
<Text key={key} color={theme.text.accent}>
|
||||
{codeMatch[2]}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
fullMatch.startsWith('[') &&
|
||||
fullMatch.includes('](') &&
|
||||
fullMatch.endsWith(')')
|
||||
) {
|
||||
const linkMatch = fullMatch.match(/\[(.*?)\]\((.*?)\)/);
|
||||
if (linkMatch) {
|
||||
const linkText = linkMatch[1];
|
||||
const url = linkMatch[2];
|
||||
renderedNode = (
|
||||
<Text key={key} color={baseColor}>
|
||||
{linkText}
|
||||
<Text color={theme.text.link}> ({url})</Text>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
fullMatch.startsWith('<u>') &&
|
||||
fullMatch.endsWith('</u>') &&
|
||||
fullMatch.length >
|
||||
UNDERLINE_TAG_START_LENGTH + UNDERLINE_TAG_END_LENGTH - 1 // -1 because length is compared to combined length of start and end tags
|
||||
) {
|
||||
renderedNode = (
|
||||
<Text key={key} underline color={baseColor}>
|
||||
{fullMatch.slice(
|
||||
UNDERLINE_TAG_START_LENGTH,
|
||||
-UNDERLINE_TAG_END_LENGTH,
|
||||
)}
|
||||
</Text>
|
||||
);
|
||||
} else if (fullMatch.match(/^https?:\/\//)) {
|
||||
renderedNode = (
|
||||
<Text key={key} color={theme.text.link}>
|
||||
{fullMatch}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugLogger.warn('Error parsing inline markdown part:', fullMatch, e);
|
||||
renderedNode = null;
|
||||
}
|
||||
|
||||
nodes.push(
|
||||
renderedNode ?? (
|
||||
<Text key={key} color={baseColor}>
|
||||
{fullMatch}
|
||||
</Text>
|
||||
),
|
||||
);
|
||||
lastIndex = inlineRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(
|
||||
<Text key={`t-${lastIndex}`} color={baseColor}>
|
||||
{text.slice(lastIndex)}
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
|
||||
return <>{nodes.filter((node) => node !== null)}</>;
|
||||
};
|
||||
|
||||
export const RenderInline = React.memo(RenderInlineInternal);
|
||||
|
||||
@@ -267,6 +267,7 @@ describe('TableRenderer', () => {
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Comprehensive Architectural');
|
||||
expect(output).toContain('protocol buffers');
|
||||
expect(output).toContain('exponential backoff');
|
||||
@@ -377,134 +378,4 @@ describe('TableRenderer', () => {
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'renders complex markdown in rows and calculates widths correctly',
|
||||
headers: ['Feature', 'Markdown'],
|
||||
rows: [
|
||||
['Bold', '**Bold Text**'],
|
||||
['Italic', '_Italic Text_'],
|
||||
['Combined', '***Bold and Italic***'],
|
||||
['Link', '[Google](https://google.com)'],
|
||||
['Code', '`const x = 1`'],
|
||||
['Strikethrough', '~~Strike~~'],
|
||||
['Underline', '<u>Underline</u>'],
|
||||
],
|
||||
terminalWidth: 80,
|
||||
waitForText: 'Bold Text',
|
||||
assertions: (output: string) => {
|
||||
expect(output).not.toContain('**Bold Text**');
|
||||
expect(output).toContain('Bold Text');
|
||||
expect(output).not.toContain('_Italic Text_');
|
||||
expect(output).toContain('Italic Text');
|
||||
expect(output).toContain('Bold and Italic');
|
||||
expect(output).toContain('Google');
|
||||
expect(output).toContain('https://google.com');
|
||||
expect(output).toContain('(https://google.com)');
|
||||
expect(output).toContain('const x = 1');
|
||||
expect(output).not.toContain('`const x = 1`');
|
||||
expect(output).toContain('Strike');
|
||||
expect(output).toContain('Underline');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'calculates column widths based on rendered text, not raw markdown',
|
||||
headers: ['Col 1', 'Col 2', 'Col 3'],
|
||||
rows: [
|
||||
['**123456**', 'Normal', 'Short'],
|
||||
['Short', '**123456**', 'Normal'],
|
||||
['Normal', 'Short', '**123456**'],
|
||||
],
|
||||
terminalWidth: 40,
|
||||
waitForText: '123456',
|
||||
assertions: (output: string) => {
|
||||
expect(output).toContain('123456');
|
||||
const dataLines = output.split('\n').filter((l) => /123456/.test(l));
|
||||
expect(dataLines.length).toBe(3);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'handles nested markdown styles recursively',
|
||||
headers: ['Header 1', 'Header 2', 'Header 3'],
|
||||
rows: [
|
||||
['**Bold with _Italic_ and ~~Strike~~**', 'Normal', 'Short'],
|
||||
['Short', '**Bold with _Italic_ and ~~Strike~~**', 'Normal'],
|
||||
['Normal', 'Short', '**Bold with _Italic_ and ~~Strike~~**'],
|
||||
],
|
||||
terminalWidth: 100,
|
||||
waitForText: 'Bold with Italic and Strike',
|
||||
assertions: (output: string) => {
|
||||
expect(output).not.toContain('**');
|
||||
expect(output).not.toContain('_');
|
||||
expect(output).not.toContain('~~');
|
||||
expect(output).toContain('Bold with Italic and Strike');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'calculates width correctly for content with URLs and styles',
|
||||
headers: ['Col 1', 'Col 2', 'Col 3'],
|
||||
rows: [
|
||||
['Visit [Google](https://google.com)', 'Plain Text', 'More Info'],
|
||||
['Info Here', 'Visit [Bing](https://bing.com)', 'Links'],
|
||||
['Check This', 'Search', 'Visit [Yahoo](https://yahoo.com)'],
|
||||
],
|
||||
terminalWidth: 120,
|
||||
waitForText: 'Visit Google',
|
||||
assertions: (output: string) => {
|
||||
expect(output).toContain('Visit Google');
|
||||
expect(output).toContain('Visit Bing');
|
||||
expect(output).toContain('Visit Yahoo');
|
||||
expect(output).toContain('https://google.com');
|
||||
expect(output).toContain('https://bing.com');
|
||||
expect(output).toContain('https://yahoo.com');
|
||||
expect(output).toContain('(https://google.com)');
|
||||
const dataLine = output
|
||||
.split('\n')
|
||||
.find((l) => l.includes('Visit Google'));
|
||||
expect(dataLine).toContain('Visit Google');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'does not parse markdown inside code snippets',
|
||||
headers: ['Col 1', 'Col 2', 'Col 3'],
|
||||
rows: [
|
||||
['`**not bold**`', '`_not italic_`', '`~~not strike~~`'],
|
||||
['`[not link](url)`', '`<u>not underline</u>`', '`https://not.link`'],
|
||||
['Normal Text', 'More Code: `*test*`', '`***nested***`'],
|
||||
],
|
||||
terminalWidth: 100,
|
||||
waitForText: '**not bold**',
|
||||
assertions: (output: string) => {
|
||||
expect(output).toContain('**not bold**');
|
||||
expect(output).toContain('_not italic_');
|
||||
expect(output).toContain('~~not strike~~');
|
||||
expect(output).toContain('[not link](url)');
|
||||
expect(output).toContain('<u>not underline</u>');
|
||||
expect(output).toContain('https://not.link');
|
||||
expect(output).toContain('***nested***');
|
||||
},
|
||||
},
|
||||
])(
|
||||
'$name',
|
||||
async ({ headers, rows, terminalWidth, waitForText, assertions }) => {
|
||||
const renderResult = renderWithProviders(
|
||||
<TableRenderer
|
||||
headers={headers}
|
||||
rows={rows}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
{ width: terminalWidth },
|
||||
);
|
||||
const { lastFrame, waitUntilReady, unmount } = renderResult;
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
expect(output).toContain(waitForText);
|
||||
assertions(output);
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,19 +5,18 @@
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { styledCharsToString } from '@alcalzone/ansi-tokenize';
|
||||
import { Text, Box } from 'ink';
|
||||
import {
|
||||
Text,
|
||||
Box,
|
||||
type StyledChar,
|
||||
toStyledCharacters,
|
||||
styledCharsToString,
|
||||
styledCharsWidth,
|
||||
wordBreakStyledChars,
|
||||
wrapStyledChars,
|
||||
widestLineFromStyledChars,
|
||||
} from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { parseMarkdownToANSI } from './markdownParsingUtils.js';
|
||||
import { RenderInline } from './InlineMarkdownRenderer.js';
|
||||
import { stripUnsafeCharacters } from './textUtils.js';
|
||||
|
||||
interface TableRendererProps {
|
||||
@@ -30,19 +29,6 @@ const MIN_COLUMN_WIDTH = 5;
|
||||
const COLUMN_PADDING = 2;
|
||||
const TABLE_MARGIN = 2;
|
||||
|
||||
/**
|
||||
* Parses markdown to StyledChar array 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 = (
|
||||
text: string,
|
||||
defaultColor?: string,
|
||||
): StyledChar[] => {
|
||||
const ansi = parseMarkdownToANSI(text, defaultColor);
|
||||
return toStyledCharacters(ansi);
|
||||
};
|
||||
|
||||
const calculateWidths = (styledChars: StyledChar[]) => {
|
||||
const contentWidth = styledCharsWidth(styledChars);
|
||||
|
||||
@@ -67,26 +53,25 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
rows,
|
||||
terminalWidth,
|
||||
}) => {
|
||||
// Clean headers: remove bold markers since we already render headers as bold
|
||||
// and having them can break wrapping when the markers are split across lines.
|
||||
const cleanedHeaders = useMemo(
|
||||
() => headers.map((header) => header.replace(/\*\*(.*?)\*\*/g, '$1')),
|
||||
[headers],
|
||||
);
|
||||
|
||||
const styledHeaders = useMemo(
|
||||
() =>
|
||||
headers.map((header) =>
|
||||
parseMarkdownToStyledChars(
|
||||
stripUnsafeCharacters(header),
|
||||
theme.text.link,
|
||||
),
|
||||
cleanedHeaders.map((header) =>
|
||||
toStyledCharacters(stripUnsafeCharacters(header)),
|
||||
),
|
||||
[headers],
|
||||
[cleanedHeaders],
|
||||
);
|
||||
|
||||
const styledRows = useMemo(
|
||||
() =>
|
||||
rows.map((row) =>
|
||||
row.map((cell) =>
|
||||
parseMarkdownToStyledChars(
|
||||
stripUnsafeCharacters(cell),
|
||||
theme.text.primary,
|
||||
),
|
||||
),
|
||||
row.map((cell) => toStyledCharacters(stripUnsafeCharacters(cell))),
|
||||
),
|
||||
[rows],
|
||||
);
|
||||
@@ -147,7 +132,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
|
||||
const scale =
|
||||
(availableWidth - finalTotalShortColumnWidth) /
|
||||
(totalMinWidth - finalTotalShortColumnWidth) || 0;
|
||||
(totalMinWidth - finalTotalShortColumnWidth);
|
||||
finalContentWidths = constraints.map((c) => {
|
||||
if (c.maxWidth <= MIN_COLUMN_WIDTH && finalTotalShortColumnWidth > 0) {
|
||||
return c.minWidth;
|
||||
@@ -216,7 +201,6 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
|
||||
return { wrappedHeaders, wrappedRows, adjustedWidths };
|
||||
}, [styledHeaders, styledRows, terminalWidth]);
|
||||
|
||||
// Helper function to render a cell with proper width
|
||||
const renderCell = (
|
||||
content: ProcessedLine,
|
||||
@@ -232,10 +216,10 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
<Text>
|
||||
{isHeader ? (
|
||||
<Text bold color={theme.text.link}>
|
||||
{content.text}
|
||||
<RenderInline text={content.text} />
|
||||
</Text>
|
||||
) : (
|
||||
<Text>{content.text}</Text>
|
||||
<RenderInline text={content.text} />
|
||||
)}
|
||||
{' '.repeat(paddingNeeded)}
|
||||
</Text>
|
||||
@@ -269,18 +253,18 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Text color={theme.border.default}>│</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text color={theme.border.default}>│</Text>{' '}
|
||||
{renderedCells.map((cell, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Box paddingX={1}>{cell}</Box>
|
||||
{cell}
|
||||
{index < renderedCells.length - 1 && (
|
||||
<Text color={theme.border.default}>│</Text>
|
||||
<Text color={theme.border.default}>{' │ '}</Text>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
))}{' '}
|
||||
<Text color={theme.border.default}>│</Text>
|
||||
</Box>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -290,7 +274,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
rowIndex?: number,
|
||||
isHeader = false,
|
||||
): React.ReactNode => {
|
||||
const key = rowIndex === -1 ? 'header' : `${rowIndex}`;
|
||||
const key = isHeader ? 'header' : `${rowIndex}`;
|
||||
const maxHeight = Math.max(...wrappedCells.map((lines) => lines.length), 1);
|
||||
|
||||
const visualRows: React.ReactNode[] = [];
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`InlineMarkdownRenderer > RenderInline > handles nested/complex markdown gracefully (best effort) 1`] = `
|
||||
"Bold *Italic
|
||||
*"
|
||||
`;
|
||||
|
||||
exports[`InlineMarkdownRenderer > RenderInline > renders bold text correctly 1`] = `
|
||||
"Hello
|
||||
World"
|
||||
`;
|
||||
|
||||
exports[`InlineMarkdownRenderer > RenderInline > renders inline code correctly 1`] = `
|
||||
"Hello
|
||||
World"
|
||||
`;
|
||||
|
||||
exports[`InlineMarkdownRenderer > RenderInline > renders italic text correctly 1`] = `
|
||||
"Hello
|
||||
World"
|
||||
`;
|
||||
|
||||
exports[`InlineMarkdownRenderer > RenderInline > renders links correctly 1`] = `"Google (https://google.com)"`;
|
||||
|
||||
exports[`InlineMarkdownRenderer > RenderInline > renders mixed markdown correctly 1`] = `
|
||||
"Bold
|
||||
and
|
||||
Italic
|
||||
and
|
||||
Code
|
||||
and
|
||||
Link (https://example.com)"
|
||||
`;
|
||||
|
||||
exports[`InlineMarkdownRenderer > RenderInline > renders plain text correctly 1`] = `"Hello World"`;
|
||||
|
||||
exports[`InlineMarkdownRenderer > RenderInline > renders raw URLs correctly 1`] = `
|
||||
"Visit
|
||||
https://google.com"
|
||||
`;
|
||||
|
||||
exports[`InlineMarkdownRenderer > RenderInline > renders strikethrough text correctly 1`] = `
|
||||
"Hello
|
||||
World"
|
||||
`;
|
||||
|
||||
exports[`InlineMarkdownRenderer > RenderInline > renders underline correctly 1`] = `
|
||||
"Hello
|
||||
World"
|
||||
`;
|
||||
|
||||
exports[`InlineMarkdownRenderer > RenderInline > respects defaultColor prop 1`] = `"Hello"`;
|
||||
@@ -1,39 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="380" height="156" viewBox="0 0 380 156">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="380" height="156" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="19" fill="#333333" textLength="252" lengthAdjust="spacingAndGlyphs">┌────────┬────────┬────────┐</text>
|
||||
<text x="0" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="36" fill="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Col 1</text>
|
||||
<text x="81" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="99" y="36" fill="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Col 2</text>
|
||||
<text x="162" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="180" y="36" fill="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Col 3</text>
|
||||
<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="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="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="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="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="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="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="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="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="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>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1,45 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1100" height="156" viewBox="0 0 1100 156">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="1100" height="156" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="19" fill="#333333" textLength="927" lengthAdjust="spacingAndGlyphs">┌───────────────────────────────────┬───────────────────────────────┬─────────────────────────────────┐</text>
|
||||
<text x="0" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="36" fill="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Col 1</text>
|
||||
<text x="324" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="342" y="36" fill="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Col 2</text>
|
||||
<text x="612" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="630" y="36" fill="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Col 3</text>
|
||||
<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="9" y="70" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs"> Visit Google (</text>
|
||||
<text x="144" y="70" fill="#89b4fa" textLength="162" lengthAdjust="spacingAndGlyphs">https://google.com</text>
|
||||
<text x="306" y="70" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">) </text>
|
||||
<text x="324" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</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="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="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="333" y="87" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> Visit Bing (</text>
|
||||
<text x="450" y="87" fill="#89b4fa" textLength="144" lengthAdjust="spacingAndGlyphs">https://bing.com</text>
|
||||
<text x="594" y="87" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">) </text>
|
||||
<text x="612" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</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="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="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="621" y="104" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Visit Yahoo (</text>
|
||||
<text x="747" y="104" fill="#89b4fa" textLength="153" lengthAdjust="spacingAndGlyphs">https://yahoo.com</text>
|
||||
<text x="900" y="104" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">) </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>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.1 KiB |
@@ -1,40 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="156" viewBox="0 0 920 156">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="156" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="19" fill="#333333" textLength="549" lengthAdjust="spacingAndGlyphs">┌─────────────────┬──────────────────────┬──────────────────┐</text>
|
||||
<text x="0" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="36" fill="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Col 1</text>
|
||||
<text x="162" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="180" y="36" fill="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Col 2</text>
|
||||
<text x="369" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="387" y="36" fill="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Col 3</text>
|
||||
<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="#cba6f7" textLength="108" lengthAdjust="spacingAndGlyphs">**not bold**</text>
|
||||
<text x="162" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="180" y="70" fill="#cba6f7" textLength="108" lengthAdjust="spacingAndGlyphs">_not italic_</text>
|
||||
<text x="369" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="387" y="70" fill="#cba6f7" textLength="126" 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="#cba6f7" textLength="135" lengthAdjust="spacingAndGlyphs">[not link](url)</text>
|
||||
<text x="162" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="180" y="87" fill="#cba6f7" textLength="180" 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="#cba6f7" textLength="144" 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="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="171" y="104" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> More Code: </text>
|
||||
<text x="279" y="104" fill="#cba6f7" textLength="54" lengthAdjust="spacingAndGlyphs">*test*</text>
|
||||
<text x="369" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="387" y="104" fill="#cba6f7" textLength="108" 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>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB |
@@ -1,39 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="156" viewBox="0 0 920 156">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="156" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="19" fill="#333333" textLength="819" lengthAdjust="spacingAndGlyphs">┌─────────────────────────────┬─────────────────────────────┬─────────────────────────────┐</text>
|
||||
<text x="0" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="36" fill="#89b4fa" textLength="72" lengthAdjust="spacingAndGlyphs">Header 1</text>
|
||||
<text x="270" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="288" y="36" fill="#89b4fa" textLength="72" lengthAdjust="spacingAndGlyphs">Header 2</text>
|
||||
<text x="540" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="558" y="36" fill="#89b4fa" textLength="72" lengthAdjust="spacingAndGlyphs">Header 3</text>
|
||||
<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="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="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="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="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="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="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="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="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="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>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
@@ -7,25 +7,25 @@
|
||||
<text x="0" y="19" fill="#333333" textLength="405" lengthAdjust="spacingAndGlyphs">┌──────────────┬────────────┬───────────────┐</text>
|
||||
<text x="0" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="36" fill="#89b4fa" textLength="63" lengthAdjust="spacingAndGlyphs">Emoji 😃</text>
|
||||
<text x="126" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="117" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="144" y="36" fill="#89b4fa" textLength="90" lengthAdjust="spacingAndGlyphs">Asian 汉字</text>
|
||||
<text x="243" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="234" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="261" y="36" fill="#89b4fa" textLength="108" lengthAdjust="spacingAndGlyphs">Mixed 🚀 Text</text>
|
||||
<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="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="135" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> 你好世界 </text>
|
||||
<text x="243" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="252" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Rocket 🚀 Man </text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> Start 🌟 End</text>
|
||||
<text x="117" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="144" y="70" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">你好世界 </text>
|
||||
<text x="234" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="261" y="70" fill="#ffffff" textLength="117" 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="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="135" y="87" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> こんにちは </text>
|
||||
<text x="243" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="252" y="87" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Fire 🔥 </text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> Thumbs 👍 Up</text>
|
||||
<text x="117" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="144" y="87" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">こんにちは</text>
|
||||
<text x="234" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="261" y="87" fill="#ffffff" textLength="117" 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.1 KiB After Width: | Height: | Size: 3.1 KiB |
@@ -7,40 +7,40 @@
|
||||
<text x="0" y="19" fill="#333333" textLength="297" lengthAdjust="spacingAndGlyphs">┌─────────────┬───────┬─────────┐</text>
|
||||
<text x="0" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="36" fill="#89b4fa" textLength="81" lengthAdjust="spacingAndGlyphs">Very Long</text>
|
||||
<text x="126" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="117" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="144" y="36" fill="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Short</text>
|
||||
<text x="198" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="189" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="216" y="36" fill="#89b4fa" textLength="63" lengthAdjust="spacingAndGlyphs">Another</text>
|
||||
<text x="288" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="53" fill="#89b4fa" textLength="99" lengthAdjust="spacingAndGlyphs">Bold Header</text>
|
||||
<text x="126" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="198" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="117" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="189" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="216" y="53" fill="#89b4fa" textLength="36" lengthAdjust="spacingAndGlyphs">Long</text>
|
||||
<text x="288" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#89b4fa" textLength="81" lengthAdjust="spacingAndGlyphs">That Will</text>
|
||||
<text x="126" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="198" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="117" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="189" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="216" y="70" fill="#89b4fa" textLength="54" lengthAdjust="spacingAndGlyphs">Header</text>
|
||||
<text x="288" 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="#89b4fa" textLength="36" lengthAdjust="spacingAndGlyphs">Wrap</text>
|
||||
<text x="126" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="198" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="117" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="189" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<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="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="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="207" y="121" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs"> Data 3 </text>
|
||||
<text x="9" y="121" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> Data 1 </text>
|
||||
<text x="117" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="144" y="121" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">Data </text>
|
||||
<text x="189" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="216" y="121" fill="#ffffff" textLength="72" 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="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="117" y="138" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="144" y="138" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">2 </text>
|
||||
<text x="189" y="138" fill="#333333" textLength="27" 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>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -7,32 +7,32 @@
|
||||
<text x="0" y="19" fill="#333333" textLength="414" lengthAdjust="spacingAndGlyphs">┌──────────────┬──────────────┬──────────────┐</text>
|
||||
<text x="0" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="36" fill="#89b4fa" textLength="72" lengthAdjust="spacingAndGlyphs">Header 1</text>
|
||||
<text x="135" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="126" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="153" y="36" fill="#89b4fa" textLength="72" lengthAdjust="spacingAndGlyphs">Header 2</text>
|
||||
<text x="270" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="261" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="36" fill="#89b4fa" textLength="72" lengthAdjust="spacingAndGlyphs">Header 3</text>
|
||||
<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="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="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="279" y="70" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 1, Col 3 </text>
|
||||
<text x="9" y="70" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> Row 1, Col 1</text>
|
||||
<text x="126" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="153" y="70" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 1, Col 2</text>
|
||||
<text x="261" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="70" fill="#ffffff" textLength="117" 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="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="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="279" y="87" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 2, Col 3 </text>
|
||||
<text x="9" y="87" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> Row 2, Col 1</text>
|
||||
<text x="126" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="153" y="87" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 2, Col 2</text>
|
||||
<text x="261" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="87" fill="#ffffff" textLength="117" 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="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="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="279" y="104" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Row 3, Col 3 </text>
|
||||
<text x="9" y="104" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> Row 3, Col 1</text>
|
||||
<text x="126" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="153" y="104" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Row 3, Col 2</text>
|
||||
<text x="261" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="104" fill="#ffffff" textLength="117" 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.8 KiB After Width: | Height: | Size: 3.8 KiB |
@@ -7,394 +7,394 @@
|
||||
<text x="0" y="19" fill="#333333" textLength="1404" lengthAdjust="spacingAndGlyphs">┌─────────────────────────────┬──────────────────────────────┬─────────────────────────────┬──────────────────────────────┬─────┬────────┬─────────┬───────┐</text>
|
||||
<text x="0" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="36" fill="#89b4fa" textLength="243" lengthAdjust="spacingAndGlyphs">Comprehensive Architectural</text>
|
||||
<text x="270" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="261" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="36" fill="#89b4fa" textLength="234" lengthAdjust="spacingAndGlyphs">Implementation Details for</text>
|
||||
<text x="549" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="540" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="36" fill="#89b4fa" textLength="216" lengthAdjust="spacingAndGlyphs">Longitudinal Performance</text>
|
||||
<text x="819" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="810" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="36" fill="#89b4fa" textLength="252" lengthAdjust="spacingAndGlyphs">Strategic Security Framework</text>
|
||||
<text x="1098" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1089" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1116" y="36" fill="#89b4fa" textLength="27" lengthAdjust="spacingAndGlyphs">Key</text>
|
||||
<text x="1152" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1143" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1170" y="36" fill="#89b4fa" textLength="54" lengthAdjust="spacingAndGlyphs">Status</text>
|
||||
<text x="1233" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1224" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1251" y="36" fill="#89b4fa" textLength="63" lengthAdjust="spacingAndGlyphs">Version</text>
|
||||
<text x="1323" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1314" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1341" y="36" fill="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Owner</text>
|
||||
<text x="1395" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="53" fill="#89b4fa" textLength="189" lengthAdjust="spacingAndGlyphs">Specification for the</text>
|
||||
<text x="270" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="261" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="53" fill="#89b4fa" textLength="171" lengthAdjust="spacingAndGlyphs">the High-Throughput</text>
|
||||
<text x="549" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="540" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="53" fill="#89b4fa" textLength="135" lengthAdjust="spacingAndGlyphs">Analysis Across</text>
|
||||
<text x="819" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="810" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="53" fill="#89b4fa" textLength="252" lengthAdjust="spacingAndGlyphs">for Mitigating Sophisticated</text>
|
||||
<text x="1098" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1089" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1395" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#89b4fa" textLength="234" lengthAdjust="spacingAndGlyphs">Distributed Infrastructure</text>
|
||||
<text x="270" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="261" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="70" fill="#89b4fa" textLength="180" lengthAdjust="spacingAndGlyphs">Asynchronous Message</text>
|
||||
<text x="549" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="540" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="70" fill="#89b4fa" textLength="180" lengthAdjust="spacingAndGlyphs">Multi-Regional Cloud</text>
|
||||
<text x="819" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="810" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="70" fill="#89b4fa" textLength="180" lengthAdjust="spacingAndGlyphs">Cross-Site Scripting</text>
|
||||
<text x="1098" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1089" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1395" 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="#89b4fa" textLength="45" lengthAdjust="spacingAndGlyphs">Layer</text>
|
||||
<text x="270" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="261" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="87" fill="#89b4fa" textLength="216" lengthAdjust="spacingAndGlyphs">Processing Pipeline with</text>
|
||||
<text x="549" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="540" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="87" fill="#89b4fa" textLength="171" lengthAdjust="spacingAndGlyphs">Deployment Clusters</text>
|
||||
<text x="819" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="810" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="87" fill="#89b4fa" textLength="135" lengthAdjust="spacingAndGlyphs">Vulnerabilities</text>
|
||||
<text x="1098" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1089" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="87" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1395" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="270" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="261" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="104" fill="#89b4fa" textLength="180" lengthAdjust="spacingAndGlyphs">Extended Scalability</text>
|
||||
<text x="549" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="819" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1098" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="540" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="810" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1089" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1395" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="270" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="261" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="121" fill="#89b4fa" textLength="207" lengthAdjust="spacingAndGlyphs">Features and Redundancy</text>
|
||||
<text x="549" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="819" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1098" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="540" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="810" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1089" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1395" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="270" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="261" y="138" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="138" fill="#89b4fa" textLength="81" lengthAdjust="spacingAndGlyphs">Protocols</text>
|
||||
<text x="549" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="819" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1098" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="540" y="138" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="810" y="138" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1089" y="138" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="138" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="138" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="138" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<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="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="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="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="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="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="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="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="1332" y="172" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs"> J. </text>
|
||||
<text x="9" y="172" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> The primary architecture </text>
|
||||
<text x="261" y="172" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="172" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Each message is processed </text>
|
||||
<text x="540" y="172" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="172" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Historical data indicates a</text>
|
||||
<text x="810" y="172" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="172" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">A multi-layered defense </text>
|
||||
<text x="1089" y="172" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1116" y="172" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">INF</text>
|
||||
<text x="1143" y="172" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1170" y="172" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Active</text>
|
||||
<text x="1224" y="172" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1251" y="172" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">v2.4 </text>
|
||||
<text x="1314" y="172" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1341" y="172" fill="#ffffff" textLength="54" 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="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="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="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="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="1332" y="189" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs"> Doe </text>
|
||||
<text x="9" y="189" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> utilizes a decoupled </text>
|
||||
<text x="261" y="189" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="189" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">through a series of </text>
|
||||
<text x="540" y="189" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="189" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">significant reduction in </text>
|
||||
<text x="810" y="189" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="189" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">strategy incorporates </text>
|
||||
<text x="1089" y="189" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="189" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="189" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="189" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1341" y="189" fill="#ffffff" textLength="54" 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="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="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="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="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="9" y="206" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> microservices approach, </text>
|
||||
<text x="261" y="206" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="206" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">specialized workers that </text>
|
||||
<text x="540" y="206" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="206" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">tail latency when utilizing</text>
|
||||
<text x="810" y="206" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="206" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">content security policies, </text>
|
||||
<text x="1089" y="206" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="206" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="206" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="206" fill="#333333" textLength="27" 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="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="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="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="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="9" y="223" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> leveraging container </text>
|
||||
<text x="261" y="223" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="223" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">handle data transformation, </text>
|
||||
<text x="540" y="223" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="223" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">edge computing nodes closer</text>
|
||||
<text x="810" y="223" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="223" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">input sanitization </text>
|
||||
<text x="1089" y="223" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="223" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="223" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="223" fill="#333333" textLength="27" 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="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="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="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="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="9" y="240" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> orchestration for </text>
|
||||
<text x="261" y="240" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="240" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">validation, and persistent </text>
|
||||
<text x="540" y="240" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="240" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">to the geographic location </text>
|
||||
<text x="810" y="240" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="240" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">libraries, and regular </text>
|
||||
<text x="1089" y="240" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="240" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="240" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="240" fill="#333333" textLength="27" 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="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="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="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="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="9" y="257" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> scalability and fault </text>
|
||||
<text x="261" y="257" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="257" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">storage using a persistent </text>
|
||||
<text x="540" y="257" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="257" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">of the end-user base. </text>
|
||||
<text x="810" y="257" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="257" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">automated penetration </text>
|
||||
<text x="1089" y="257" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="257" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="257" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="257" fill="#333333" textLength="27" 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="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="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="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="9" y="274" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> tolerance in high-load </text>
|
||||
<text x="261" y="274" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="274" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">queue. </text>
|
||||
<text x="540" y="274" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="810" y="274" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="274" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">testing routines. </text>
|
||||
<text x="1089" y="274" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="274" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="274" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="274" fill="#333333" textLength="27" 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="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="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>
|
||||
<text x="1233" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="291" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> scenarios. </text>
|
||||
<text x="261" y="291" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="540" y="291" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="291" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Monitoring tools have </text>
|
||||
<text x="810" y="291" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1089" y="291" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="291" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="291" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="291" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<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="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="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="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="261" y="308" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="308" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">The pipeline features </text>
|
||||
<text x="540" y="308" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="308" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">captured a steady increase </text>
|
||||
<text x="810" y="308" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="308" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Developers are required to </text>
|
||||
<text x="1089" y="308" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="308" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="308" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="308" fill="#333333" textLength="27" 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="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="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="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="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="9" y="325" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> This layer provides the </text>
|
||||
<text x="261" y="325" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="325" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">built-in retry mechanisms </text>
|
||||
<text x="540" y="325" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="325" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">in throughput efficiency </text>
|
||||
<text x="810" y="325" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="325" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">undergo mandatory security </text>
|
||||
<text x="1089" y="325" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="325" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="325" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="325" fill="#333333" textLength="27" 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="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="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="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="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="9" y="342" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> fundamental building blocks</text>
|
||||
<text x="261" y="342" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="342" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">with exponential backoff to </text>
|
||||
<text x="540" y="342" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="342" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">since the introduction of </text>
|
||||
<text x="810" y="342" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="342" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">training focusing on the </text>
|
||||
<text x="1089" y="342" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="342" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="342" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="342" fill="#333333" textLength="27" 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="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="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="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="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="9" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> for service discovery, load</text>
|
||||
<text x="261" y="359" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">ensure message delivery </text>
|
||||
<text x="540" y="359" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="359" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">the vectorized query engine</text>
|
||||
<text x="810" y="359" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">OWASP Top Ten to ensure that</text>
|
||||
<text x="1089" y="359" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="359" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="359" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="359" fill="#333333" textLength="27" 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="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="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="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="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="9" y="376" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> balancing, and </text>
|
||||
<text x="261" y="376" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="376" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">integrity even during </text>
|
||||
<text x="540" y="376" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="376" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">in the primary data </text>
|
||||
<text x="810" y="376" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="376" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">security is integrated into </text>
|
||||
<text x="1089" y="376" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="376" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="376" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="376" fill="#333333" textLength="27" 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="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="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="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="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="9" y="393" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> inter-service communication</text>
|
||||
<text x="261" y="393" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="393" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">transient network or service</text>
|
||||
<text x="540" y="393" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="393" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">warehouse. </text>
|
||||
<text x="810" y="393" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="393" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">the initial design phase. </text>
|
||||
<text x="1089" y="393" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="393" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="393" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="393" fill="#333333" textLength="27" 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="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="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>
|
||||
<text x="1152" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="410" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> via highly efficient </text>
|
||||
<text x="261" y="410" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="410" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">failures. </text>
|
||||
<text x="540" y="410" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="810" y="410" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1089" y="410" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="410" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="410" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="410" fill="#333333" textLength="27" 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="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="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="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>
|
||||
<text x="1323" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="427" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> protocol buffers. </text>
|
||||
<text x="261" y="427" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="540" y="427" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="427" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Resource utilization </text>
|
||||
<text x="810" y="427" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="427" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">The implementation of a </text>
|
||||
<text x="1089" y="427" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="427" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="427" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="427" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<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="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="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="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="261" y="444" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="444" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Horizontal autoscaling is </text>
|
||||
<text x="540" y="444" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="444" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">metrics demonstrate that </text>
|
||||
<text x="810" y="444" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="444" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">robust Identity and Access </text>
|
||||
<text x="1089" y="444" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="444" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="444" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="444" fill="#333333" textLength="27" 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="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="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="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="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="9" y="461" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> Advanced telemetry and </text>
|
||||
<text x="261" y="461" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="461" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">triggered automatically </text>
|
||||
<text x="540" y="461" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="461" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">the transition to </text>
|
||||
<text x="810" y="461" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="461" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Management system ensures </text>
|
||||
<text x="1089" y="461" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="461" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="461" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="461" fill="#333333" textLength="27" 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="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="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="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="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="9" y="478" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> logging integrations allow </text>
|
||||
<text x="261" y="478" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="478" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">based on the depth of the </text>
|
||||
<text x="540" y="478" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="478" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">serverless compute for </text>
|
||||
<text x="810" y="478" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="478" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">that the principle of least </text>
|
||||
<text x="1089" y="478" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="478" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="478" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="478" fill="#333333" textLength="27" 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="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="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="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="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="9" y="495" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> for real-time monitoring of</text>
|
||||
<text x="261" y="495" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="495" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">processing queue, ensuring </text>
|
||||
<text x="540" y="495" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="495" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">intermittent tasks has </text>
|
||||
<text x="810" y="495" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="495" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">privilege is strictly </text>
|
||||
<text x="1089" y="495" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="495" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="495" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="495" fill="#333333" textLength="27" 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="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="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="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="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="9" y="512" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> system health and rapid </text>
|
||||
<text x="261" y="512" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="512" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">consistent performance </text>
|
||||
<text x="540" y="512" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="512" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">resulted in a thirty </text>
|
||||
<text x="810" y="512" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="512" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">enforced across all </text>
|
||||
<text x="1089" y="512" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="512" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="512" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="512" fill="#333333" textLength="27" 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="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="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="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="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="9" y="529" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> identification of </text>
|
||||
<text x="261" y="529" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="529" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">during unexpected traffic </text>
|
||||
<text x="540" y="529" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="567" y="529" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">percent cost optimization. </text>
|
||||
<text x="810" y="529" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="837" y="529" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">environments. </text>
|
||||
<text x="1089" y="529" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="529" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="529" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="529" fill="#333333" textLength="27" 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="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="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>
|
||||
<text x="1152" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="546" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> bottlenecks within the </text>
|
||||
<text x="261" y="546" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="288" y="546" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">spikes. </text>
|
||||
<text x="540" y="546" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="810" y="546" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1089" y="546" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="546" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="546" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="546" fill="#333333" textLength="27" 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="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>
|
||||
<text x="1098" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1152" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1233" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="1323" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="563" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs"> service mesh. </text>
|
||||
<text x="261" y="563" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="540" y="563" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="810" y="563" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1089" y="563" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1143" y="563" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1224" y="563" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1314" y="563" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="1395" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="580" fill="#333333" textLength="1404" lengthAdjust="spacingAndGlyphs">└─────────────────────────────┴──────────────────────────────┴─────────────────────────────┴──────────────────────────────┴─────┴────────┴─────────┴───────┘</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
@@ -7,56 +7,56 @@
|
||||
<text x="0" y="19" fill="#333333" textLength="639" lengthAdjust="spacingAndGlyphs">┌───────────────┬───────────────┬──────────────────┬──────────────────┐</text>
|
||||
<text x="0" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="36" fill="#89b4fa" textLength="81" lengthAdjust="spacingAndGlyphs">Very Long</text>
|
||||
<text x="144" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="135" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="162" y="36" fill="#89b4fa" textLength="81" lengthAdjust="spacingAndGlyphs">Very Long</text>
|
||||
<text x="288" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="279" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="306" y="36" fill="#89b4fa" textLength="144" lengthAdjust="spacingAndGlyphs">Very Long Column</text>
|
||||
<text x="459" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="450" y="36" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="477" y="36" fill="#89b4fa" textLength="144" lengthAdjust="spacingAndGlyphs">Very Long Column</text>
|
||||
<text x="630" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="53" fill="#89b4fa" textLength="117" lengthAdjust="spacingAndGlyphs">Column Header</text>
|
||||
<text x="144" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="135" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="162" y="53" fill="#89b4fa" textLength="117" lengthAdjust="spacingAndGlyphs">Column Header</text>
|
||||
<text x="288" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="279" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="306" y="53" fill="#89b4fa" textLength="108" lengthAdjust="spacingAndGlyphs">Header Three</text>
|
||||
<text x="459" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="450" y="53" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="477" y="53" fill="#89b4fa" textLength="99" lengthAdjust="spacingAndGlyphs">Header Four</text>
|
||||
<text x="630" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#89b4fa" textLength="27" lengthAdjust="spacingAndGlyphs">One</text>
|
||||
<text x="144" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="135" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="162" y="70" fill="#89b4fa" textLength="27" lengthAdjust="spacingAndGlyphs">Two</text>
|
||||
<text x="288" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="459" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="279" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="450" y="70" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<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="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="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="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="468" y="104" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> Data 1.4 </text>
|
||||
<text x="9" y="104" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Data 1.1 </text>
|
||||
<text x="135" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="162" y="104" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs">Data 1.2 </text>
|
||||
<text x="279" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="306" y="104" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Data 1.3 </text>
|
||||
<text x="450" y="104" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="477" y="104" fill="#ffffff" textLength="153" 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="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="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="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="468" y="121" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> Data 2.4 </text>
|
||||
<text x="9" y="121" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Data 2.1 </text>
|
||||
<text x="135" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="162" y="121" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs">Data 2.2 </text>
|
||||
<text x="279" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="306" y="121" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Data 2.3 </text>
|
||||
<text x="450" y="121" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="477" y="121" fill="#ffffff" textLength="153" 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="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="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="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="468" y="138" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs"> Data 3.4 </text>
|
||||
<text x="9" y="138" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs"> Data 3.1 </text>
|
||||
<text x="135" y="138" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="162" y="138" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs">Data 3.2 </text>
|
||||
<text x="279" y="138" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="306" y="138" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Data 3.3 </text>
|
||||
<text x="450" y="138" fill="#333333" textLength="27" lengthAdjust="spacingAndGlyphs"> │ </text>
|
||||
<text x="477" y="138" fill="#ffffff" textLength="153" 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.5 KiB After Width: | Height: | Size: 6.5 KiB |