mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-27 02:01:00 -07:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0f32ff7d6 | |||
| 1693afe027 | |||
| d179e3673b | |||
| e4d6a5cc7c | |||
| fc4fdc3cbe | |||
| bdbb996a2e |
@@ -1,45 +0,0 @@
|
||||
---
|
||||
name: async-pr-review
|
||||
description: Trigger this skill when the user wants to start an asynchronous PR review, run background checks on a PR, or check the status of a previously started async PR review.
|
||||
---
|
||||
|
||||
# Async PR Review
|
||||
|
||||
This skill provides a set of tools to asynchronously review a Pull Request. It will create a background job to run the project's preflight checks, execute Gemini-powered test plans, and perform a comprehensive code review using custom prompts.
|
||||
|
||||
This skill is designed to showcase an advanced "Agentic Asynchronous Pattern":
|
||||
1. **Native Background Shells vs Headless Inference**: While Gemini CLI can natively spawn and detach background shell commands (using the `run_shell_command` tool with `is_background: true`), a standard bash background job cannot perform LLM inference. To conduct AI-driven code reviews and test generation in the background, the shell script *must* invoke the `gemini` executable headlessly using `-p`. This offloads the AI tasks to independent worker agents.
|
||||
2. **Dynamic Git Scoping**: The review scripts avoid hardcoded paths. They use `git rev-parse --show-toplevel` to automatically resolve the root of the user's current project.
|
||||
3. **Ephemeral Worktrees**: Instead of checking out branches in the user's main workspace, the skill provisions temporary git worktrees in `.gemini/tmp/async-reviews/pr-<number>`. This prevents git lock conflicts and namespace pollution.
|
||||
4. **Agentic Evaluation (`check-async-review.sh`)**: The check script outputs clean JSON/text statuses for the main agent to parse. The interactive agent itself synthesizes the final assessment dynamically from the generated log files.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Determine Action**: Establish whether the user wants to start a new async review or check the status of an existing one.
|
||||
* If the user says "start an async review for PR #123" or similar, proceed to **Start Review**.
|
||||
* If the user says "check the status of my async review for PR #123" or similar, proceed to **Check Status**.
|
||||
|
||||
### Start Review
|
||||
|
||||
If the user wants to start a new async PR review:
|
||||
|
||||
1. Ask the user for the PR number if they haven't provided it.
|
||||
2. Execute the `async-review.sh` script, passing the PR number as the first argument. Be sure to run it with the `is_background` flag set to true to ensure it immediately detaches.
|
||||
```bash
|
||||
.gemini/skills/async-pr-review/scripts/async-review.sh <PR_NUMBER>
|
||||
```
|
||||
3. Inform the user that the tasks have started successfully and they can check the status later.
|
||||
|
||||
### Check Status
|
||||
|
||||
If the user wants to check the status or view the final assessment of a previously started async review:
|
||||
|
||||
1. Ask the user for the PR number if they haven't provided it.
|
||||
2. Execute the `check-async-review.sh` script, passing the PR number as the first argument:
|
||||
```bash
|
||||
.gemini/skills/async-pr-review/scripts/check-async-review.sh <PR_NUMBER>
|
||||
```
|
||||
3. **Evaluate Output**: Read the output from the script.
|
||||
* If the output contains `STATUS: IN_PROGRESS`, tell the user which tasks are still running.
|
||||
* If the output contains `STATUS: COMPLETE`, use your file reading tools (`read_file`) to retrieve the contents of `final-assessment.md`, `review.md`, `pr-diff.diff`, `npm-test.log`, and `test-execution.log` files from the `LOG_DIR` specified in the output.
|
||||
* **Final Assessment**: Read those files, synthesize their results, and give the user a concise recommendation on whether the PR builds successfully, passes tests, and if you recommend they approve it based on the review.
|
||||
@@ -1,148 +0,0 @@
|
||||
# --- CORE TOOLS ---
|
||||
[[rule]]
|
||||
toolName = "read_file"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "write_file"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "grep_search"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "glob"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "list_directory"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "codebase_investigator"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
# --- SHELL COMMANDS ---
|
||||
|
||||
# Git (Safe/Read-only)
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = [
|
||||
"git blame",
|
||||
"git show",
|
||||
"git grep",
|
||||
"git show-ref",
|
||||
"git ls-tree",
|
||||
"git ls-remote",
|
||||
"git reflog",
|
||||
"git remote -v",
|
||||
"git diff",
|
||||
"git rev-list",
|
||||
"git rev-parse",
|
||||
"git merge-base",
|
||||
"git cherry",
|
||||
"git fetch",
|
||||
"git status",
|
||||
"git st",
|
||||
"git branch",
|
||||
"git br",
|
||||
"git log",
|
||||
"git --version"
|
||||
]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
# GitHub CLI (Read-only)
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = [
|
||||
"gh workflow list",
|
||||
"gh auth status",
|
||||
"gh checkout view",
|
||||
"gh run view",
|
||||
"gh run job view",
|
||||
"gh run list",
|
||||
"gh run --help",
|
||||
"gh issue view",
|
||||
"gh issue list",
|
||||
"gh label list",
|
||||
"gh pr diff",
|
||||
"gh pr check",
|
||||
"gh pr checks",
|
||||
"gh pr view",
|
||||
"gh pr list",
|
||||
"gh pr status",
|
||||
"gh repo view",
|
||||
"gh job view",
|
||||
"gh api",
|
||||
"gh log"
|
||||
]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
# Node.js/NPM (Generic Tests, Checks, and Build)
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = [
|
||||
"npm run start",
|
||||
"npm install",
|
||||
"npm run",
|
||||
"npm test",
|
||||
"npm ci",
|
||||
"npm list",
|
||||
"npm --version"
|
||||
]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
# Core Utilities (Safe)
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = [
|
||||
"sleep",
|
||||
"env",
|
||||
"break",
|
||||
"xargs",
|
||||
"base64",
|
||||
"uniq",
|
||||
"sort",
|
||||
"echo",
|
||||
"which",
|
||||
"ls",
|
||||
"find",
|
||||
"tail",
|
||||
"head",
|
||||
"cat",
|
||||
"cd",
|
||||
"grep",
|
||||
"ps",
|
||||
"pwd",
|
||||
"wc",
|
||||
"file",
|
||||
"stat",
|
||||
"diff",
|
||||
"lsof",
|
||||
"date",
|
||||
"whoami",
|
||||
"uname",
|
||||
"du",
|
||||
"cut",
|
||||
"true",
|
||||
"false",
|
||||
"readlink",
|
||||
"awk",
|
||||
"jq",
|
||||
"rg",
|
||||
"less",
|
||||
"more",
|
||||
"tree"
|
||||
]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
@@ -1,241 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
notify() {
|
||||
local title="$1"
|
||||
local message="$2"
|
||||
local pr="$3"
|
||||
# Terminal escape sequence
|
||||
printf "\e]9;%s | PR #%s | %s\a" "$title" "$pr" "$message"
|
||||
# Native macOS notification
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
osascript -e "display notification \"$message\" with title \"$title\" subtitle \"PR #$pr\""
|
||||
fi
|
||||
}
|
||||
|
||||
pr_number=$1
|
||||
if [[ -z "$pr_number" ]]; then
|
||||
echo "Usage: async-review <pr_number>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
base_dir=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
if [[ -z "$base_dir" ]]; then
|
||||
echo "❌ Must be run from within a git repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use the repository's local .gemini/tmp directory for ephemeral worktrees and logs
|
||||
pr_dir="$base_dir/.gemini/tmp/async-reviews/pr-$pr_number"
|
||||
target_dir="$pr_dir/worktree"
|
||||
log_dir="$pr_dir/logs"
|
||||
|
||||
cd "$base_dir" || exit 1
|
||||
|
||||
mkdir -p "$log_dir"
|
||||
rm -f "$log_dir/setup.exit" "$log_dir/final-assessment.exit" "$log_dir/final-assessment.md"
|
||||
|
||||
echo "🧹 Cleaning up previous worktree if it exists..." | tee -a "$log_dir/setup.log"
|
||||
git worktree remove -f "$target_dir" >> "$log_dir/setup.log" 2>&1 || true
|
||||
git branch -D "gemini-async-pr-$pr_number" >> "$log_dir/setup.log" 2>&1 || true
|
||||
git worktree prune >> "$log_dir/setup.log" 2>&1 || true
|
||||
|
||||
echo "📡 Fetching PR #$pr_number..." | tee -a "$log_dir/setup.log"
|
||||
if ! git fetch origin -f "pull/$pr_number/head:gemini-async-pr-$pr_number" >> "$log_dir/setup.log" 2>&1; then
|
||||
echo 1 > "$log_dir/setup.exit"
|
||||
echo "❌ Fetch failed. Check $log_dir/setup.log"
|
||||
notify "Async Review Failed" "Fetch failed." "$pr_number"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$target_dir" ]]; then
|
||||
echo "🧹 Pruning missing worktrees..." | tee -a "$log_dir/setup.log"
|
||||
git worktree prune >> "$log_dir/setup.log" 2>&1
|
||||
echo "🌿 Creating worktree in $target_dir..." | tee -a "$log_dir/setup.log"
|
||||
if ! git worktree add "$target_dir" "gemini-async-pr-$pr_number" >> "$log_dir/setup.log" 2>&1; then
|
||||
echo 1 > "$log_dir/setup.exit"
|
||||
echo "❌ Worktree creation failed. Check $log_dir/setup.log"
|
||||
notify "Async Review Failed" "Worktree creation failed." "$pr_number"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "🌿 Worktree already exists." | tee -a "$log_dir/setup.log"
|
||||
fi
|
||||
echo 0 > "$log_dir/setup.exit"
|
||||
|
||||
cd "$target_dir" || exit 1
|
||||
|
||||
echo "🚀 Launching background tasks. Logs saving to: $log_dir"
|
||||
|
||||
echo " ↳ [1/5] Grabbing PR diff..."
|
||||
rm -f "$log_dir/pr-diff.exit"
|
||||
{ gh pr diff "$pr_number" > "$log_dir/pr-diff.diff" 2>&1; echo $? > "$log_dir/pr-diff.exit"; } &
|
||||
|
||||
echo " ↳ [2/5] Starting build and lint..."
|
||||
rm -f "$log_dir/build-and-lint.exit"
|
||||
{ { npm run clean && npm ci && npm run format && npm run build && npm run lint:ci && npm run typecheck; } > "$log_dir/build-and-lint.log" 2>&1; echo $? > "$log_dir/build-and-lint.exit"; } &
|
||||
|
||||
# Dynamically resolve gemini binary (fallback to your nightly path)
|
||||
GEMINI_CMD=$(which gemini || echo "$HOME/.gcli/nightly/node_modules/.bin/gemini")
|
||||
POLICY_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/policy.toml"
|
||||
|
||||
echo " ↳ [3/5] Starting Gemini code review..."
|
||||
rm -f "$log_dir/review.exit"
|
||||
{ "$GEMINI_CMD" --policy "$POLICY_PATH" -p "/review-frontend $pr_number" > "$log_dir/review.md" 2>&1; echo $? > "$log_dir/review.exit"; } &
|
||||
|
||||
echo " ↳ [4/5] Starting automated tests (waiting for build and lint)..."
|
||||
rm -f "$log_dir/npm-test.exit"
|
||||
{
|
||||
while [ ! -f "$log_dir/build-and-lint.exit" ]; do sleep 1; done
|
||||
if [ "$(cat "$log_dir/build-and-lint.exit")" == "0" ]; then
|
||||
gh pr checks "$pr_number" > "$log_dir/ci-checks.log" 2>&1
|
||||
ci_status=$?
|
||||
|
||||
if [ "$ci_status" -eq 0 ]; then
|
||||
echo "CI checks passed. Skipping local npm tests." > "$log_dir/npm-test.log"
|
||||
echo 0 > "$log_dir/npm-test.exit"
|
||||
elif [ "$ci_status" -eq 8 ]; then
|
||||
echo "CI checks are still pending. Skipping local npm tests to avoid duplicate work. Please check GitHub for final results." > "$log_dir/npm-test.log"
|
||||
echo 0 > "$log_dir/npm-test.exit"
|
||||
else
|
||||
echo "CI checks failed. Failing checks:" > "$log_dir/npm-test.log"
|
||||
gh pr checks "$pr_number" --json name,bucket -q '.[] | select(.bucket=="fail") | .name' >> "$log_dir/npm-test.log" 2>&1
|
||||
|
||||
echo "Attempting to extract failing test files from CI logs..." >> "$log_dir/npm-test.log"
|
||||
pr_branch=$(gh pr view "$pr_number" --json headRefName -q '.headRefName' 2>/dev/null)
|
||||
run_id=$(gh run list --branch "$pr_branch" --workflow ci.yml --json databaseId -q '.[0].databaseId' 2>/dev/null)
|
||||
|
||||
failed_files=""
|
||||
if [[ -n "$run_id" ]]; then
|
||||
failed_files=$(gh run view "$run_id" --log-failed 2>/dev/null | grep -o -E '(packages/[a-zA-Z0-9_-]+|integration-tests|evals)/[a-zA-Z0-9_/-]+\.test\.ts(x)?' | sort | uniq)
|
||||
fi
|
||||
|
||||
if [[ -n "$failed_files" ]]; then
|
||||
echo "Found failing test files from CI:" >> "$log_dir/npm-test.log"
|
||||
for f in $failed_files; do echo " - $f" >> "$log_dir/npm-test.log"; done
|
||||
echo "Running ONLY failing tests locally..." >> "$log_dir/npm-test.log"
|
||||
|
||||
exit_code=0
|
||||
for file in $failed_files; do
|
||||
if [[ "$file" == packages/* ]]; then
|
||||
ws_dir=$(echo "$file" | cut -d'/' -f1,2)
|
||||
else
|
||||
ws_dir=$(echo "$file" | cut -d'/' -f1)
|
||||
fi
|
||||
rel_file=${file#$ws_dir/}
|
||||
|
||||
echo "--- Running $rel_file in workspace $ws_dir ---" >> "$log_dir/npm-test.log"
|
||||
if ! npm run test:ci -w "$ws_dir" -- "$rel_file" >> "$log_dir/npm-test.log" 2>&1; then
|
||||
exit_code=1
|
||||
fi
|
||||
done
|
||||
echo $exit_code > "$log_dir/npm-test.exit"
|
||||
else
|
||||
echo "Could not extract specific failing files. Skipping full local test suite as it takes too long. Please check CI logs manually." >> "$log_dir/npm-test.log"
|
||||
echo 1 > "$log_dir/npm-test.exit"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Skipped due to build-and-lint failure" > "$log_dir/npm-test.log"
|
||||
echo 1 > "$log_dir/npm-test.exit"
|
||||
fi
|
||||
} &
|
||||
|
||||
echo " ↳ [5/5] Starting Gemini test execution (waiting for build and lint)..."
|
||||
rm -f "$log_dir/test-execution.exit"
|
||||
{
|
||||
while [ ! -f "$log_dir/build-and-lint.exit" ]; do sleep 1; done
|
||||
if [ "$(cat "$log_dir/build-and-lint.exit")" == "0" ]; then
|
||||
"$GEMINI_CMD" --policy "$POLICY_PATH" -p "Analyze the diff for PR $pr_number using 'gh pr diff $pr_number'. Instead of running the project's automated test suite (like 'npm test'), physically exercise the newly changed code in the terminal (e.g., by writing a temporary script to call the new functions, or testing the CLI command directly). Verify the feature's behavior works as expected. IMPORTANT: Do NOT modify any source code to fix errors. Just exercise the code and log the results, reporting any failures clearly. Do not ask for user confirmation." > "$log_dir/test-execution.log" 2>&1; echo $? > "$log_dir/test-execution.exit"
|
||||
else
|
||||
echo "Skipped due to build-and-lint failure" > "$log_dir/test-execution.log"
|
||||
echo 1 > "$log_dir/test-execution.exit"
|
||||
fi
|
||||
} &
|
||||
|
||||
echo "✅ All tasks dispatched!"
|
||||
echo "You can monitor progress with: tail -f $log_dir/*.log"
|
||||
echo "Read your review later at: $log_dir/review.md"
|
||||
|
||||
# Polling loop to wait for all background tasks to finish
|
||||
tasks=("pr-diff" "build-and-lint" "review" "npm-test" "test-execution")
|
||||
log_files=("pr-diff.diff" "build-and-lint.log" "review.md" "npm-test.log" "test-execution.log")
|
||||
|
||||
declare -A task_done
|
||||
for t in "${tasks[@]}"; do task_done[$t]=0; done
|
||||
|
||||
all_done=0
|
||||
while [[ $all_done -eq 0 ]]; do
|
||||
clear
|
||||
echo "=================================================="
|
||||
echo "🚀 Async PR Review Status for PR #$pr_number"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
|
||||
all_done=1
|
||||
for i in "${!tasks[@]}"; do
|
||||
t="${tasks[$i]}"
|
||||
|
||||
if [[ -f "$log_dir/$t.exit" ]]; then
|
||||
exit_code=$(cat "$log_dir/$t.exit")
|
||||
if [[ "$exit_code" == "0" ]]; then
|
||||
echo " ✅ $t: SUCCESS"
|
||||
else
|
||||
echo " ❌ $t: FAILED (exit code $exit_code)"
|
||||
fi
|
||||
task_done[$t]=1
|
||||
else
|
||||
echo " ⏳ $t: RUNNING"
|
||||
all_done=0
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=================================================="
|
||||
echo "📝 Live Logs (Last 5 lines of running tasks)"
|
||||
echo "=================================================="
|
||||
|
||||
for i in "${!tasks[@]}"; do
|
||||
t="${tasks[$i]}"
|
||||
log_file="${log_files[$i]}"
|
||||
|
||||
if [[ ${task_done[$t]} -eq 0 ]]; then
|
||||
if [[ -f "$log_dir/$log_file" ]]; then
|
||||
echo ""
|
||||
echo "--- $t ---"
|
||||
tail -n 5 "$log_dir/$log_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $all_done -eq 0 ]]; then
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
|
||||
clear
|
||||
echo "=================================================="
|
||||
echo "🚀 Async PR Review Status for PR #$pr_number"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
for t in "${tasks[@]}"; do
|
||||
exit_code=$(cat "$log_dir/$t.exit")
|
||||
if [[ "$exit_code" == "0" ]]; then
|
||||
echo " ✅ $t: SUCCESS"
|
||||
else
|
||||
echo " ❌ $t: FAILED (exit code $exit_code)"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
echo "⏳ Tasks complete! Synthesizing final assessment..."
|
||||
if ! "$GEMINI_CMD" --policy "$POLICY_PATH" -p "Read the review at $log_dir/review.md, the automated test logs at $log_dir/npm-test.log, and the manual test execution logs at $log_dir/test-execution.log. Summarize the results, state whether the build and tests passed based on $log_dir/build-and-lint.exit and $log_dir/npm-test.exit, and give a final recommendation for PR $pr_number." > "$log_dir/final-assessment.md" 2>&1; then
|
||||
echo $? > "$log_dir/final-assessment.exit"
|
||||
echo "❌ Final assessment synthesis failed!"
|
||||
echo "Check $log_dir/final-assessment.md for details."
|
||||
notify "Async Review Failed" "Final assessment synthesis failed." "$pr_number"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo 0 > "$log_dir/final-assessment.exit"
|
||||
echo "✅ Final assessment complete! Check $log_dir/final-assessment.md"
|
||||
notify "Async Review Complete" "Review and test execution finished successfully." "$pr_number"
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/bash
|
||||
pr_number=$1
|
||||
|
||||
if [[ -z "$pr_number" ]]; then
|
||||
echo "Usage: check-async-review <pr_number>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
base_dir=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
if [[ -z "$base_dir" ]]; then
|
||||
echo "❌ Must be run from within a git repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_dir="$base_dir/.gemini/tmp/async-reviews/pr-$pr_number/logs"
|
||||
|
||||
if [[ ! -d "$log_dir" ]]; then
|
||||
echo "STATUS: NOT_FOUND"
|
||||
echo "❌ No logs found for PR #$pr_number in $log_dir"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tasks=(
|
||||
"setup|setup.log"
|
||||
"pr-diff|pr-diff.diff"
|
||||
"build-and-lint|build-and-lint.log"
|
||||
"review|review.md"
|
||||
"npm-test|npm-test.log"
|
||||
"test-execution|test-execution.log"
|
||||
"final-assessment|final-assessment.md"
|
||||
)
|
||||
|
||||
all_done=true
|
||||
echo "STATUS: CHECKING"
|
||||
|
||||
for task_info in "${tasks[@]}"; do
|
||||
IFS="|" read -r task_name log_file <<< "$task_info"
|
||||
|
||||
file_path="$log_dir/$log_file"
|
||||
exit_file="$log_dir/$task_name.exit"
|
||||
|
||||
if [[ -f "$exit_file" ]]; then
|
||||
exit_code=$(cat "$exit_file")
|
||||
if [[ "$exit_code" == "0" ]]; then
|
||||
echo "✅ $task_name: SUCCESS"
|
||||
else
|
||||
echo "❌ $task_name: FAILED (exit code $exit_code)"
|
||||
echo " Last lines of $file_path:"
|
||||
tail -n 3 "$file_path" | sed 's/^/ /'
|
||||
fi
|
||||
elif [[ -f "$file_path" ]]; then
|
||||
echo "⏳ $task_name: RUNNING"
|
||||
all_done=false
|
||||
else
|
||||
echo "➖ $task_name: NOT STARTED"
|
||||
all_done=false
|
||||
fi
|
||||
done
|
||||
|
||||
if $all_done; then
|
||||
echo "STATUS: COMPLETE"
|
||||
echo "LOG_DIR: $log_dir"
|
||||
else
|
||||
echo "STATUS: IN_PROGRESS"
|
||||
fi
|
||||
@@ -125,9 +125,7 @@ they appear in the UI.
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
|
||||
| Tool Sandboxing | `security.toolSandboxing` | Experimental tool-level sandboxing (implementation in progress). | `false` |
|
||||
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
||||
| Disable Always Allow | `security.disableAlwaysAllow` | Disable "Always allow" options in tool confirmation dialogs. | `false` |
|
||||
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
|
||||
| Auto-add to Policy by Default | `security.autoAddToPolicyByDefault` | When enabled, the "Allow for all future sessions" option becomes the default choice for low-risk tools in trusted workspaces. | `false` |
|
||||
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
|
||||
@@ -152,7 +150,6 @@ they appear in the UI.
|
||||
| Plan | `experimental.plan` | Enable Plan Mode. | `true` |
|
||||
| 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` |
|
||||
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
|
||||
|
||||
### Skills
|
||||
|
||||
|
||||
@@ -25,20 +25,6 @@ To use remote subagents, you must explicitly enable them in your
|
||||
}
|
||||
```
|
||||
|
||||
## Proxy support
|
||||
|
||||
Gemini CLI routes traffic to remote agents through an HTTP/HTTPS proxy if one is
|
||||
configured. It uses the `general.proxy` setting in your `settings.json` file or
|
||||
standard environment variables (`HTTP_PROXY`, `HTTPS_PROXY`).
|
||||
|
||||
```json
|
||||
{
|
||||
"general": {
|
||||
"proxy": "http://my-proxy:8080"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Defining remote subagents
|
||||
|
||||
Remote subagents are defined as Markdown files (`.md`) with YAML frontmatter.
|
||||
@@ -54,7 +40,6 @@ You can place them in:
|
||||
| `kind` | string | Yes | Must be `remote`. |
|
||||
| `name` | string | Yes | A unique name for the agent. Must be a valid slug (lowercase letters, numbers, hyphens, and underscores only). |
|
||||
| `agent_card_url` | string | Yes | The URL to the agent's A2A card endpoint. |
|
||||
| `auth` | object | No | Authentication configuration. See [Authentication](#authentication). |
|
||||
|
||||
### Single-subagent example
|
||||
|
||||
@@ -85,273 +70,6 @@ Markdown file.
|
||||
> **Note:** Mixed local and remote agents, or multiple local agents, are not
|
||||
> supported in a single file; the list format is currently remote-only.
|
||||
|
||||
## Authentication
|
||||
|
||||
Many remote agents require authentication. Gemini CLI supports several
|
||||
authentication methods aligned with the
|
||||
[A2A security specification](https://a2a-protocol.org/latest/specification/#451-securityscheme).
|
||||
Add an `auth` block to your agent's frontmatter to configure credentials.
|
||||
|
||||
### Supported auth types
|
||||
|
||||
Gemini CLI supports the following authentication types:
|
||||
|
||||
| Type | Description |
|
||||
| :------------------- | :--------------------------------------------------------------------------------------------- |
|
||||
| `apiKey` | Send a static API key as an HTTP header. |
|
||||
| `http` | HTTP authentication (Bearer token, Basic credentials, or any IANA-registered scheme). |
|
||||
| `google-credentials` | Google Application Default Credentials (ADC). Automatically selects access or identity tokens. |
|
||||
| `oauth2` | OAuth 2.0 Authorization Code flow with PKCE. Opens a browser for interactive sign-in. |
|
||||
|
||||
### Dynamic values
|
||||
|
||||
For `apiKey` and `http` auth types, secret values (`key`, `token`, `username`,
|
||||
`password`, `value`) support dynamic resolution:
|
||||
|
||||
| Format | Description | Example |
|
||||
| :---------- | :-------------------------------------------------- | :------------------------- |
|
||||
| `$ENV_VAR` | Read from an environment variable. | `$MY_API_KEY` |
|
||||
| `!command` | Execute a shell command and use the trimmed output. | `!gcloud auth print-token` |
|
||||
| literal | Use the string as-is. | `sk-abc123` |
|
||||
| `$$` / `!!` | Escape prefix. `$$FOO` becomes the literal `$FOO`. | `$$NOT_AN_ENV_VAR` |
|
||||
|
||||
> **Security tip:** Prefer `$ENV_VAR` or `!command` over embedding secrets
|
||||
> directly in agent files, especially for project-level agents checked into
|
||||
> version control.
|
||||
|
||||
### API key (`apiKey`)
|
||||
|
||||
Sends an API key as an HTTP header on every request.
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :----- | :----- | :------- | :---------------------------------------------------- |
|
||||
| `type` | string | Yes | Must be `apiKey`. |
|
||||
| `key` | string | Yes | The API key value. Supports dynamic values. |
|
||||
| `name` | string | No | Header name to send the key in. Default: `X-API-Key`. |
|
||||
|
||||
```yaml
|
||||
---
|
||||
kind: remote
|
||||
name: my-agent
|
||||
agent_card_url: https://example.com/agent-card
|
||||
auth:
|
||||
type: apiKey
|
||||
key: $MY_API_KEY
|
||||
---
|
||||
```
|
||||
|
||||
### HTTP authentication (`http`)
|
||||
|
||||
Supports Bearer tokens, Basic auth, and arbitrary IANA-registered HTTP
|
||||
authentication schemes.
|
||||
|
||||
#### Bearer token
|
||||
|
||||
Use the following fields to configure a Bearer token:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :------- | :----- | :------- | :----------------------------------------- |
|
||||
| `type` | string | Yes | Must be `http`. |
|
||||
| `scheme` | string | Yes | Must be `Bearer`. |
|
||||
| `token` | string | Yes | The bearer token. Supports dynamic values. |
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
type: http
|
||||
scheme: Bearer
|
||||
token: $MY_BEARER_TOKEN
|
||||
```
|
||||
|
||||
#### Basic authentication
|
||||
|
||||
Use the following fields to configure Basic authentication:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :--------- | :----- | :------- | :------------------------------------- |
|
||||
| `type` | string | Yes | Must be `http`. |
|
||||
| `scheme` | string | Yes | Must be `Basic`. |
|
||||
| `username` | string | Yes | The username. Supports dynamic values. |
|
||||
| `password` | string | Yes | The password. Supports dynamic values. |
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
type: http
|
||||
scheme: Basic
|
||||
username: $MY_USERNAME
|
||||
password: $MY_PASSWORD
|
||||
```
|
||||
|
||||
#### Raw scheme
|
||||
|
||||
For any other IANA-registered scheme (for example, Digest, HOBA), provide the
|
||||
raw authorization value.
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :------- | :----- | :------- | :---------------------------------------------------------------------------- |
|
||||
| `type` | string | Yes | Must be `http`. |
|
||||
| `scheme` | string | Yes | The scheme name (for example, `Digest`). |
|
||||
| `value` | string | Yes | Raw value sent as `Authorization: <scheme> <value>`. Supports dynamic values. |
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
type: http
|
||||
scheme: Digest
|
||||
value: $MY_DIGEST_VALUE
|
||||
```
|
||||
|
||||
### Google Application Default Credentials (`google-credentials`)
|
||||
|
||||
Uses
|
||||
[Google Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials)
|
||||
to authenticate with Google Cloud services and Cloud Run endpoints. This is the
|
||||
recommended auth method for agents hosted on Google Cloud infrastructure.
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :------- | :------- | :------- | :-------------------------------------------------------------------------- |
|
||||
| `type` | string | Yes | Must be `google-credentials`. |
|
||||
| `scopes` | string[] | No | OAuth scopes. Defaults to `https://www.googleapis.com/auth/cloud-platform`. |
|
||||
|
||||
```yaml
|
||||
---
|
||||
kind: remote
|
||||
name: my-gcp-agent
|
||||
agent_card_url: https://my-agent-xyz.run.app/.well-known/agent.json
|
||||
auth:
|
||||
type: google-credentials
|
||||
---
|
||||
```
|
||||
|
||||
#### How token selection works
|
||||
|
||||
The provider automatically selects the correct token type based on the agent's
|
||||
host:
|
||||
|
||||
| Host pattern | Token type | Use case |
|
||||
| :----------------- | :----------------- | :------------------------------------------ |
|
||||
| `*.googleapis.com` | **Access token** | Google APIs (Agent Engine, Vertex AI, etc.) |
|
||||
| `*.run.app` | **Identity token** | Cloud Run services |
|
||||
|
||||
- **Access tokens** authorize API calls to Google services. They are scoped
|
||||
(default: `cloud-platform`) and fetched via `GoogleAuth.getClient()`.
|
||||
- **Identity tokens** prove the caller's identity to a service that validates
|
||||
the token's audience. The audience is set to the target host. These are
|
||||
fetched via `GoogleAuth.getIdTokenClient()`.
|
||||
|
||||
Both token types are cached and automatically refreshed before expiry.
|
||||
|
||||
#### Setup
|
||||
|
||||
`google-credentials` relies on ADC, which means your environment must have
|
||||
credentials configured. Common setups:
|
||||
|
||||
- **Local development:** Run `gcloud auth application-default login` to
|
||||
authenticate with your Google account.
|
||||
- **CI / Cloud environments:** Use a service account. Set the
|
||||
`GOOGLE_APPLICATION_CREDENTIALS` environment variable to the path of your
|
||||
service account key file, or use workload identity on GKE / Cloud Run.
|
||||
|
||||
#### Allowed hosts
|
||||
|
||||
For security, `google-credentials` only sends tokens to known Google-owned
|
||||
hosts:
|
||||
|
||||
- `*.googleapis.com`
|
||||
- `*.run.app`
|
||||
|
||||
Requests to any other host will be rejected with an error. If your agent is
|
||||
hosted on a different domain, use one of the other auth types (`apiKey`, `http`,
|
||||
or `oauth2`).
|
||||
|
||||
#### Examples
|
||||
|
||||
The following examples demonstrate how to configure Google Application Default
|
||||
Credentials.
|
||||
|
||||
**Cloud Run agent:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
kind: remote
|
||||
name: cloud-run-agent
|
||||
agent_card_url: https://my-agent-xyz.run.app/.well-known/agent.json
|
||||
auth:
|
||||
type: google-credentials
|
||||
---
|
||||
```
|
||||
|
||||
**Google API with custom scopes:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
kind: remote
|
||||
name: vertex-agent
|
||||
agent_card_url: https://us-central1-aiplatform.googleapis.com/.well-known/agent.json
|
||||
auth:
|
||||
type: google-credentials
|
||||
scopes:
|
||||
- https://www.googleapis.com/auth/cloud-platform
|
||||
- https://www.googleapis.com/auth/compute
|
||||
---
|
||||
```
|
||||
|
||||
### OAuth 2.0 (`oauth2`)
|
||||
|
||||
Performs an interactive OAuth 2.0 Authorization Code flow with PKCE. On first
|
||||
use, Gemini CLI opens your browser for sign-in and persists the resulting tokens
|
||||
for subsequent requests.
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :------------------ | :------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | string | Yes | Must be `oauth2`. |
|
||||
| `client_id` | string | Yes\* | OAuth client ID. Required for interactive auth. |
|
||||
| `client_secret` | string | No\* | OAuth client secret. Required by most authorization servers (confidential clients). Can be omitted for public clients that don't require a secret. |
|
||||
| `scopes` | string[] | No | Requested scopes. Can also be discovered from the agent card. |
|
||||
| `authorization_url` | string | No | Authorization endpoint. Discovered from the agent card if omitted. |
|
||||
| `token_url` | string | No | Token endpoint. Discovered from the agent card if omitted. |
|
||||
|
||||
```yaml
|
||||
---
|
||||
kind: remote
|
||||
name: oauth-agent
|
||||
agent_card_url: https://example.com/.well-known/agent.json
|
||||
auth:
|
||||
type: oauth2
|
||||
client_id: my-client-id.apps.example.com
|
||||
---
|
||||
```
|
||||
|
||||
If the agent card advertises an `oauth2` security scheme with
|
||||
`authorizationCode` flow, the `authorization_url`, `token_url`, and `scopes` are
|
||||
automatically discovered. You only need to provide `client_id` (and
|
||||
`client_secret` if required).
|
||||
|
||||
Tokens are persisted to disk and refreshed automatically when they expire.
|
||||
|
||||
### Auth validation
|
||||
|
||||
When Gemini CLI loads a remote agent, it validates your auth configuration
|
||||
against the agent card's declared `securitySchemes`. If the agent requires
|
||||
authentication that you haven't configured, you'll see an error describing
|
||||
what's needed.
|
||||
|
||||
`google-credentials` is treated as compatible with `http` Bearer security
|
||||
schemes, since it produces Bearer tokens.
|
||||
|
||||
### Auth retry behavior
|
||||
|
||||
All auth providers automatically retry on `401` and `403` responses by
|
||||
re-fetching credentials (up to 2 retries). This handles cases like expired
|
||||
tokens or rotated credentials. For `apiKey` with `!command` values, the command
|
||||
is re-executed on retry to fetch a fresh key.
|
||||
|
||||
### Agent card fetching and auth
|
||||
|
||||
When connecting to a remote agent, Gemini CLI first fetches the agent card
|
||||
**without** authentication. If the card endpoint returns a `401` or `403`, it
|
||||
retries the fetch **with** the configured auth headers. This lets agents have
|
||||
publicly accessible cards while protecting their task endpoints, or to protect
|
||||
both behind auth.
|
||||
|
||||
## Managing Subagents
|
||||
|
||||
Users can manage subagents using the following commands within the Gemini CLI:
|
||||
|
||||
+19
-135
@@ -38,34 +38,6 @@ main agent calls the tool, it delegates the task to the subagent. Once the
|
||||
subagent completes its task, it reports back to the main agent with its
|
||||
findings.
|
||||
|
||||
## How to use subagents
|
||||
|
||||
You can use subagents through automatic delegation or by explicitly forcing them
|
||||
in your prompt.
|
||||
|
||||
### Automatic delegation
|
||||
|
||||
Gemini CLI's main agent is instructed to use specialized subagents when a task
|
||||
matches their expertise. For example, if you ask "How does the auth system
|
||||
work?", the main agent may decide to call the `codebase_investigator` subagent
|
||||
to perform the research.
|
||||
|
||||
### Forcing a subagent (@ syntax)
|
||||
|
||||
You can explicitly direct a task to a specific subagent by using the `@` symbol
|
||||
followed by the subagent's name at the beginning of your prompt. This is useful
|
||||
when you want to bypass the main agent's decision-making and go straight to a
|
||||
specialist.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
@codebase_investigator Map out the relationship between the AgentRegistry and the LocalAgentExecutor.
|
||||
```
|
||||
|
||||
When you use the `@` syntax, the CLI injects a system note that nudges the
|
||||
primary model to use that specific subagent tool immediately.
|
||||
|
||||
## Built-in subagents
|
||||
|
||||
Gemini CLI comes with the following built-in subagents:
|
||||
@@ -77,17 +49,15 @@ Gemini CLI comes with the following built-in subagents:
|
||||
dependencies.
|
||||
- **When to use:** "How does the authentication system work?", "Map out the
|
||||
dependencies of the `AgentRegistry` class."
|
||||
- **Configuration:** Enabled by default. You can override its settings in
|
||||
`settings.json` under `agents.overrides`. Example (forcing a specific model
|
||||
and increasing turns):
|
||||
- **Configuration:** Enabled by default. You can configure it in
|
||||
`settings.json`. Example (forcing a specific model):
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"overrides": {
|
||||
"codebase_investigator": {
|
||||
"modelConfig": { "model": "gemini-3-flash-preview" },
|
||||
"runConfig": { "maxTurns": 50 }
|
||||
}
|
||||
"experimental": {
|
||||
"codebaseInvestigatorSettings": {
|
||||
"enabled": true,
|
||||
"maxNumTurns": 20,
|
||||
"model": "gemini-2.5-pro"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,7 +233,7 @@ kind: local
|
||||
tools:
|
||||
- read_file
|
||||
- grep_search
|
||||
model: gemini-3-flash-preview
|
||||
model: gemini-2.5-pro
|
||||
temperature: 0.2
|
||||
max_turns: 10
|
||||
---
|
||||
@@ -284,102 +254,16 @@ it yourself; just report it.
|
||||
|
||||
### Configuration schema
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :------------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `name` | string | Yes | Unique identifier (slug) used as the tool name for the agent. Only lowercase letters, numbers, hyphens, and underscores. |
|
||||
| `description` | string | Yes | Short description of what the agent does. This is visible to the main agent to help it decide when to call this subagent. |
|
||||
| `kind` | string | No | `local` (default) or `remote`. |
|
||||
| `tools` | array | No | List of tool names this agent can use. Supports wildcards: `*` (all tools), `mcp_*` (all MCP tools), `mcp_server_*` (all tools from a server). **If omitted, it inherits all tools from the parent session.** |
|
||||
| `model` | string | No | Specific model to use (e.g., `gemini-3-preview`). Defaults to `inherit` (uses the main session model). |
|
||||
| `temperature` | number | No | Model temperature (0.0 - 2.0). Defaults to `1`. |
|
||||
| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `30`. |
|
||||
| `timeout_mins` | number | No | Maximum execution time in minutes. Defaults to `10`. |
|
||||
|
||||
### Tool wildcards
|
||||
|
||||
When defining `tools` for a subagent, you can use wildcards to quickly grant
|
||||
access to groups of tools:
|
||||
|
||||
- `*`: Grant access to all available built-in and discovered tools.
|
||||
- `mcp_*`: Grant access to all tools from all connected MCP servers.
|
||||
- `mcp_my-server_*`: Grant access to all tools from a specific MCP server named
|
||||
`my-server`.
|
||||
|
||||
### Isolation and recursion protection
|
||||
|
||||
Each subagent runs in its own isolated context loop. This means:
|
||||
|
||||
- **Independent history:** The subagent's conversation history does not bloat
|
||||
the main agent's context.
|
||||
- **Isolated tools:** The subagent only has access to the tools you explicitly
|
||||
grant it.
|
||||
- **Recursion protection:** To prevent infinite loops and excessive token usage,
|
||||
subagents **cannot** call other subagents. If a subagent is granted the `*`
|
||||
tool wildcard, it will still be unable to see or invoke other agents.
|
||||
|
||||
## Managing subagents
|
||||
|
||||
You can manage subagents interactively using the `/agents` command or
|
||||
persistently via `settings.json`.
|
||||
|
||||
### Interactive management (/agents)
|
||||
|
||||
If you are in an interactive CLI session, you can use the `/agents` command to
|
||||
manage subagents without editing configuration files manually. This is the
|
||||
recommended way to quickly enable, disable, or re-configure agents on the fly.
|
||||
|
||||
For a full list of sub-commands and usage, see the
|
||||
[`/agents` command reference](../reference/commands.md#agents).
|
||||
|
||||
### Persistent configuration (settings.json)
|
||||
|
||||
While the `/agents` command and agent definition files provide a starting point,
|
||||
you can use `settings.json` for global, persistent overrides. This is useful for
|
||||
enforcing specific models or execution limits across all sessions.
|
||||
|
||||
#### `agents.overrides`
|
||||
|
||||
Use this to enable or disable specific agents or override their run
|
||||
configurations.
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"overrides": {
|
||||
"security-auditor": {
|
||||
"enabled": false,
|
||||
"runConfig": {
|
||||
"maxTurns": 20,
|
||||
"maxTimeMinutes": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `modelConfigs.overrides`
|
||||
|
||||
You can target specific subagents with custom model settings (like system
|
||||
instruction prefixes or specific safety settings) using the `overrideScope`
|
||||
field.
|
||||
|
||||
```json
|
||||
{
|
||||
"modelConfigs": {
|
||||
"overrides": [
|
||||
{
|
||||
"match": { "overrideScope": "security-auditor" },
|
||||
"modelConfig": {
|
||||
"generateContentConfig": {
|
||||
"temperature": 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
| Field | Type | Required | Description |
|
||||
| :------------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `name` | string | Yes | Unique identifier (slug) used as the tool name for the agent. Only lowercase letters, numbers, hyphens, and underscores. |
|
||||
| `description` | string | Yes | Short description of what the agent does. This is visible to the main agent to help it decide when to call this subagent. |
|
||||
| `kind` | string | No | `local` (default) or `remote`. |
|
||||
| `tools` | array | No | List of tool names this agent can use. If omitted, it may have access to a default set. |
|
||||
| `model` | string | No | Specific model to use (e.g., `gemini-2.5-pro`). Defaults to `inherit` (uses the main session model). |
|
||||
| `temperature` | number | No | Model temperature (0.0 - 2.0). |
|
||||
| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `15`. |
|
||||
| `timeout_mins` | number | No | Maximum execution time in minutes. Defaults to `5`. |
|
||||
|
||||
### Optimizing your subagent
|
||||
|
||||
@@ -414,7 +298,7 @@ Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
|
||||
> **Note: Remote subagents are currently an experimental feature.**
|
||||
|
||||
See the [Remote Subagents documentation](remote-agents) for detailed
|
||||
configuration, authentication, and usage instructions.
|
||||
configuration and usage instructions.
|
||||
|
||||
## Extension subagents
|
||||
|
||||
|
||||
@@ -14,31 +14,6 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
- **Description:** Show version info. Share this information when filing issues.
|
||||
|
||||
### `/agents`
|
||||
|
||||
- **Description:** Manage local and remote subagents.
|
||||
- **Note:** This command is experimental and requires
|
||||
`experimental.enableAgents: true` in your `settings.json`.
|
||||
- **Sub-commands:**
|
||||
- **`list`**:
|
||||
- **Description:** Lists all discovered agents, including built-in, local,
|
||||
and remote agents.
|
||||
- **Usage:** `/agents list`
|
||||
- **`reload`** (alias: `refresh`):
|
||||
- **Description:** Rescans agent directories (`~/.gemini/agents` and
|
||||
`.gemini/agents`) and reloads the registry.
|
||||
- **Usage:** `/agents reload`
|
||||
- **`enable`**:
|
||||
- **Description:** Enables a specific subagent.
|
||||
- **Usage:** `/agents enable <agent-name>`
|
||||
- **`disable`**:
|
||||
- **Description:** Disables a specific subagent.
|
||||
- **Usage:** `/agents disable <agent-name>`
|
||||
- **`config`**:
|
||||
- **Description:** Opens a configuration dialog for the specified agent to
|
||||
adjust its model, temperature, or execution limits.
|
||||
- **Usage:** `/agents config <agent-name>`
|
||||
|
||||
### `/auth`
|
||||
|
||||
- **Description:** Open a dialog that lets you change the authentication method.
|
||||
|
||||
+145
-33
@@ -677,6 +677,141 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
used.
|
||||
- **Default:** `[]`
|
||||
|
||||
- **`modelConfigs.modelDefinitions`** (object):
|
||||
- **Description:** Registry of model metadata, including tier, family, and
|
||||
features.
|
||||
- **Default:**
|
||||
|
||||
```json
|
||||
{
|
||||
"gemini-3.1-pro-preview": {
|
||||
"tier": "pro",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": true
|
||||
}
|
||||
},
|
||||
"gemini-3.1-pro-preview-customtools": {
|
||||
"tier": "pro",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": true
|
||||
}
|
||||
},
|
||||
"gemini-3-pro-preview": {
|
||||
"tier": "pro",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": true
|
||||
}
|
||||
},
|
||||
"gemini-3-flash-preview": {
|
||||
"tier": "flash",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": true
|
||||
}
|
||||
},
|
||||
"gemini-2.5-pro": {
|
||||
"tier": "pro",
|
||||
"family": "gemini-2.5",
|
||||
"isPreview": false,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash": {
|
||||
"tier": "flash",
|
||||
"family": "gemini-2.5",
|
||||
"isPreview": false,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"tier": "flash-lite",
|
||||
"family": "gemini-2.5",
|
||||
"isPreview": false,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"auto": {
|
||||
"tier": "auto",
|
||||
"isPreview": true,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"pro": {
|
||||
"tier": "pro",
|
||||
"isPreview": false,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"flash": {
|
||||
"tier": "flash",
|
||||
"isPreview": false,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"flash-lite": {
|
||||
"tier": "flash-lite",
|
||||
"isPreview": false,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"auto-gemini-3": {
|
||||
"displayName": "Auto (Gemini 3)",
|
||||
"tier": "auto",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "main",
|
||||
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash",
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"auto-gemini-2.5": {
|
||||
"displayName": "Auto (Gemini 2.5)",
|
||||
"tier": "auto",
|
||||
"isPreview": false,
|
||||
"dialogLocation": "main",
|
||||
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash",
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `agents`
|
||||
|
||||
- **`agents.overrides`** (object):
|
||||
@@ -706,17 +841,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `undefined`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`agents.browser.allowedDomains`** (array):
|
||||
- **Description:** A list of allowed domains for the browser agent (e.g.,
|
||||
["github.com", "*.google.com"]).
|
||||
- **Default:**
|
||||
|
||||
```json
|
||||
["github.com", "*.google.com", "localhost"]
|
||||
```
|
||||
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`agents.browser.disableUserInput`** (boolean):
|
||||
- **Description:** Disable user input on browser window during automation.
|
||||
- **Default:** `true`
|
||||
@@ -784,10 +908,9 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
#### `tools`
|
||||
|
||||
- **`tools.sandbox`** (string):
|
||||
- **Description:** Legacy full-process sandbox execution environment. Set to a
|
||||
boolean to enable or disable the sandbox, provide a string path to a sandbox
|
||||
profile, or specify an explicit sandbox command (e.g., "docker", "podman",
|
||||
"lxc").
|
||||
- **Description:** Sandbox execution environment. Set to a boolean to enable
|
||||
or disable the sandbox, provide a string path to a sandbox profile, or
|
||||
specify an explicit sandbox command (e.g., "docker", "podman", "lxc").
|
||||
- **Default:** `undefined`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
@@ -891,22 +1014,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
#### `security`
|
||||
|
||||
- **`security.toolSandboxing`** (boolean):
|
||||
- **Description:** Experimental tool-level sandboxing (implementation in
|
||||
progress).
|
||||
- **Default:** `false`
|
||||
|
||||
- **`security.disableYoloMode`** (boolean):
|
||||
- **Description:** Disable YOLO mode, even if enabled by a flag.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`security.disableAlwaysAllow`** (boolean):
|
||||
- **Description:** Disable "Always allow" options in tool confirmation
|
||||
dialogs.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`security.enablePermanentToolApproval`** (boolean):
|
||||
- **Description:** Enable the "Allow for all future sessions" option in tool
|
||||
confirmation dialogs.
|
||||
@@ -1091,6 +1203,12 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.dynamicModelConfiguration`** (boolean):
|
||||
- **Description:** Enable dynamic model configuration (definitions,
|
||||
resolutions, and chains) via settings.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.enabled`** (boolean):
|
||||
- **Description:** Enable the Gemma Model Router (experimental). Requires a
|
||||
local endpoint serving Gemma via the Gemini API using LiteRT-LM shim.
|
||||
@@ -1108,11 +1226,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `"gemma3-1b-gpu-custom"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.topicUpdateNarration`** (boolean):
|
||||
- **Description:** Enable the experimental Topic & Update communication model
|
||||
for reduced chattiness and structured progress reporting.
|
||||
- **Default:** `false`
|
||||
|
||||
#### `skills`
|
||||
|
||||
- **`skills.enabled`** (boolean):
|
||||
@@ -1202,8 +1315,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
#### `admin`
|
||||
|
||||
- **`admin.secureModeEnabled`** (boolean):
|
||||
- **Description:** If true, disallows YOLO mode and "Always allow" options
|
||||
from being used.
|
||||
- **Description:** If true, disallows yolo mode from being used.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`admin.extensions.enabled`** (boolean):
|
||||
|
||||
@@ -111,7 +111,7 @@ describe('Answer vs. ask eval', () => {
|
||||
* Ensures that when the user asks a question about style, the agent does NOT
|
||||
* automatically modify the file.
|
||||
*/
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should not edit files when asked about style',
|
||||
prompt: 'Is app.ts following good style?',
|
||||
files: FILES,
|
||||
|
||||
+33
-72
@@ -5,62 +5,31 @@
|
||||
*/
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
import { appEvalTest, AppEvalCase } from './app-test-helper.js';
|
||||
import { EvalPolicy } from './test-helper.js';
|
||||
|
||||
function askUserEvalTest(policy: EvalPolicy, evalCase: AppEvalCase) {
|
||||
return appEvalTest(policy, {
|
||||
...evalCase,
|
||||
configOverrides: {
|
||||
...evalCase.configOverrides,
|
||||
general: {
|
||||
...evalCase.configOverrides?.general,
|
||||
approvalMode: 'default',
|
||||
enableAutoUpdate: false,
|
||||
enableAutoUpdateNotification: false,
|
||||
},
|
||||
},
|
||||
files: {
|
||||
...evalCase.files,
|
||||
},
|
||||
});
|
||||
}
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
describe('ask_user', () => {
|
||||
askUserEvalTest('USUALLY_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'Agent uses AskUser tool to present multiple choice options',
|
||||
prompt: `Use the ask_user tool to ask me what my favorite color is. Provide 3 options: red, green, or blue.`,
|
||||
setup: async (rig) => {
|
||||
rig.setBreakpoint(['ask_user']);
|
||||
},
|
||||
assert: async (rig) => {
|
||||
const confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||
expect(
|
||||
confirmation,
|
||||
'Expected a pending confirmation for ask_user tool',
|
||||
).toBeDefined();
|
||||
const wasToolCalled = await rig.waitForToolCall('ask_user');
|
||||
expect(wasToolCalled, 'Expected ask_user tool to be called').toBe(true);
|
||||
},
|
||||
});
|
||||
|
||||
askUserEvalTest('USUALLY_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'Agent uses AskUser tool to clarify ambiguous requirements',
|
||||
files: {
|
||||
'package.json': JSON.stringify({ name: 'my-app', version: '1.0.0' }),
|
||||
},
|
||||
prompt: `I want to build a new feature in this app. Ask me questions to clarify the requirements before proceeding.`,
|
||||
setup: async (rig) => {
|
||||
rig.setBreakpoint(['ask_user']);
|
||||
},
|
||||
assert: async (rig) => {
|
||||
const confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||
expect(
|
||||
confirmation,
|
||||
'Expected a pending confirmation for ask_user tool',
|
||||
).toBeDefined();
|
||||
const wasToolCalled = await rig.waitForToolCall('ask_user');
|
||||
expect(wasToolCalled, 'Expected ask_user tool to be called').toBe(true);
|
||||
},
|
||||
});
|
||||
|
||||
askUserEvalTest('USUALLY_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'Agent uses AskUser tool before performing significant ambiguous rework',
|
||||
files: {
|
||||
'packages/core/src/index.ts': '// index\nexport const version = "1.0.0";',
|
||||
@@ -70,37 +39,28 @@ describe('ask_user', () => {
|
||||
}),
|
||||
'README.md': '# Gemini CLI',
|
||||
},
|
||||
prompt: `I want to completely rewrite the core package to support the upcoming V2 architecture, but I haven't decided what that looks like yet. We need to figure out the requirements first. Can you ask me some questions to help nail down the design?`,
|
||||
setup: async (rig) => {
|
||||
rig.setBreakpoint(['enter_plan_mode', 'ask_user']);
|
||||
},
|
||||
prompt: `Refactor the entire core package to be better.`,
|
||||
assert: async (rig) => {
|
||||
// It might call enter_plan_mode first.
|
||||
let confirmation = await rig.waitForPendingConfirmation([
|
||||
'enter_plan_mode',
|
||||
'ask_user',
|
||||
]);
|
||||
expect(confirmation, 'Expected a tool call confirmation').toBeDefined();
|
||||
|
||||
if (confirmation?.name === 'enter_plan_mode') {
|
||||
rig.acceptConfirmation('enter_plan_mode');
|
||||
confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||
}
|
||||
const wasPlanModeCalled = await rig.waitForToolCall('enter_plan_mode');
|
||||
expect(wasPlanModeCalled, 'Expected enter_plan_mode to be called').toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
const wasAskUserCalled = await rig.waitForToolCall('ask_user');
|
||||
expect(
|
||||
confirmation?.toolName,
|
||||
'Expected ask_user to be called to clarify the significant rework',
|
||||
).toBe('ask_user');
|
||||
wasAskUserCalled,
|
||||
'Expected ask_user tool to be called to clarify the significant rework',
|
||||
).toBe(true);
|
||||
},
|
||||
});
|
||||
|
||||
// --- Regression Tests for Recent Fixes ---
|
||||
|
||||
// Regression test for issue #20177: Ensure the agent does not use \`ask_user\` to
|
||||
// Regression test for issue #20177: Ensure the agent does not use `ask_user` to
|
||||
// confirm shell commands. Fixed via prompt refinements and tool definition
|
||||
// updates to clarify that shell command confirmation is handled by the UI.
|
||||
// See fix: https://github.com/google-gemini/gemini-cli/pull/20504
|
||||
askUserEvalTest('USUALLY_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'Agent does NOT use AskUser to confirm shell commands',
|
||||
files: {
|
||||
'package.json': JSON.stringify({
|
||||
@@ -108,24 +68,25 @@ describe('ask_user', () => {
|
||||
}),
|
||||
},
|
||||
prompt: `Run 'npm run build' in the current directory.`,
|
||||
setup: async (rig) => {
|
||||
rig.setBreakpoint(['run_shell_command', 'ask_user']);
|
||||
},
|
||||
assert: async (rig) => {
|
||||
const confirmation = await rig.waitForPendingConfirmation([
|
||||
'run_shell_command',
|
||||
'ask_user',
|
||||
]);
|
||||
await rig.waitForTelemetryReady();
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const wasShellCalled = toolLogs.some(
|
||||
(log) => log.toolRequest.name === 'run_shell_command',
|
||||
);
|
||||
const wasAskUserCalled = toolLogs.some(
|
||||
(log) => log.toolRequest.name === 'ask_user',
|
||||
);
|
||||
|
||||
expect(
|
||||
confirmation,
|
||||
'Expected a pending confirmation for a tool',
|
||||
).toBeDefined();
|
||||
|
||||
wasShellCalled,
|
||||
'Expected run_shell_command tool to be called',
|
||||
).toBe(true);
|
||||
expect(
|
||||
confirmation?.toolName,
|
||||
wasAskUserCalled,
|
||||
'ask_user should not be called to confirm shell commands',
|
||||
).toBe('run_shell_command');
|
||||
).toBe(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { assertModelHasOutput } from '../integration-tests/test-helper.js';
|
||||
describe('Hierarchical Memory', () => {
|
||||
const conflictResolutionTest =
|
||||
'Agent follows hierarchy for contradictory instructions';
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: conflictResolutionTest,
|
||||
params: {
|
||||
settings: {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
describe('save_memory', () => {
|
||||
const TEST_PREFIX = 'Save memory test: ';
|
||||
const rememberingFavoriteColor = "Agent remembers user's favorite color";
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: rememberingFavoriteColor,
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
@@ -79,7 +79,7 @@ describe('save_memory', () => {
|
||||
|
||||
const ignoringTemporaryInformation =
|
||||
'Agent ignores temporary conversation details';
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: ignoringTemporaryInformation,
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
@@ -104,7 +104,7 @@ describe('save_memory', () => {
|
||||
});
|
||||
|
||||
const rememberingPetName = "Agent remembers user's pet's name";
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: rememberingPetName,
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"file_path":"test.txt","content":"hello"}}},{"text":"I've successfully written \"hello\" to test.txt. The file has been created with the specified content."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
@@ -203,33 +203,4 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => {
|
||||
// Should successfully complete all operations
|
||||
assertModelHasOutput(result);
|
||||
});
|
||||
|
||||
it('should handle tool confirmation for write_file without crashing', async () => {
|
||||
rig.setup('tool-confirmation', {
|
||||
fakeResponsesPath: join(
|
||||
__dirname,
|
||||
'browser-agent.confirmation.responses',
|
||||
),
|
||||
settings: {
|
||||
agents: {
|
||||
browser_agent: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const run = await rig.runInteractive({ approvalMode: 'default' });
|
||||
|
||||
await run.type('Write hello to test.txt');
|
||||
await run.type('\r');
|
||||
|
||||
await run.expectText('Allow', 15000);
|
||||
|
||||
await run.type('y');
|
||||
await run.type('\r');
|
||||
|
||||
await run.expectText('successfully written', 15000);
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+10
-34
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -2195,7 +2195,6 @@
|
||||
"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",
|
||||
@@ -2376,7 +2375,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -2426,7 +2424,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -2801,7 +2798,6 @@
|
||||
"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"
|
||||
@@ -2835,7 +2831,6 @@
|
||||
"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"
|
||||
@@ -2890,7 +2885,6 @@
|
||||
"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",
|
||||
@@ -4093,7 +4087,6 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4368,7 +4361,6 @@
|
||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.35.0",
|
||||
"@typescript-eslint/types": "8.35.0",
|
||||
@@ -5242,7 +5234,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -7774,7 +7765,6 @@
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8285,7 +8275,6 @@
|
||||
"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",
|
||||
@@ -9570,7 +9559,6 @@
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
|
||||
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -9850,7 +9838,6 @@
|
||||
"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",
|
||||
@@ -13453,7 +13440,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -13464,7 +13450,6 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -15512,7 +15497,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15736,8 +15720,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -15745,7 +15728,6 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -15905,7 +15887,6 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16128,7 +16109,6 @@
|
||||
"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",
|
||||
@@ -16242,7 +16222,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16255,7 +16234,6 @@
|
||||
"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",
|
||||
@@ -16897,7 +16875,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -16913,7 +16890,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -17028,7 +17005,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
@@ -17200,7 +17177,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
@@ -17440,7 +17417,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -17463,7 +17439,7 @@
|
||||
},
|
||||
"packages/devtools": {
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.16.0"
|
||||
@@ -17478,7 +17454,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -17495,7 +17471,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -17512,7 +17488,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260313.bb060d7a9"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260311.657f19c1f"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -104,7 +104,6 @@ export class AddMemoryCommand implements Command {
|
||||
const signal = abortController.signal;
|
||||
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
|
||||
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
|
||||
sandboxManager: context.config.sandboxManager,
|
||||
});
|
||||
await refreshMemory(context.config);
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
tmpdir,
|
||||
type Config,
|
||||
type Storage,
|
||||
NoopSandboxManager,
|
||||
type ToolRegistry,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
|
||||
@@ -98,14 +97,6 @@ export function createMockConfig(
|
||||
}),
|
||||
getGitService: vi.fn(),
|
||||
validatePathAccess: vi.fn().mockReturnValue(undefined),
|
||||
getShellExecutionConfig: vi.fn().mockReturnValue({
|
||||
sandboxManager: new NoopSandboxManager(),
|
||||
sanitizationConfig: {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
},
|
||||
}),
|
||||
...overrides,
|
||||
} as unknown as Config;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260313.bb060d7a9"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260311.657f19c1f"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
|
||||
@@ -176,7 +176,6 @@ describe('GeminiAgent', () => {
|
||||
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Mocked<Awaited<ReturnType<typeof loadCliConfig>>>;
|
||||
mockSettings = {
|
||||
merged: {
|
||||
@@ -655,7 +654,6 @@ describe('Session', () => {
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
getGitService: vi.fn().mockResolvedValue({} as GitService),
|
||||
waitForMcpInit: vi.fn(),
|
||||
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Mocked<Config>;
|
||||
mockConnection = {
|
||||
sessionUpdate: vi.fn(),
|
||||
@@ -949,61 +947,6 @@ describe('Session', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should exclude always allow options when disableAlwaysAllow is true', async () => {
|
||||
mockConfig.getDisableAlwaysAllow = vi.fn().mockReturnValue(true);
|
||||
const confirmationDetails = {
|
||||
type: 'info',
|
||||
onConfirm: vi.fn(),
|
||||
};
|
||||
mockTool.build.mockReturnValue({
|
||||
getDescription: () => 'Test Tool',
|
||||
toolLocations: () => [],
|
||||
shouldConfirmExecute: vi.fn().mockResolvedValue(confirmationDetails),
|
||||
execute: vi.fn().mockResolvedValue({ llmContent: 'Tool Result' }),
|
||||
});
|
||||
|
||||
mockConnection.requestPermission.mockResolvedValue({
|
||||
outcome: {
|
||||
outcome: 'selected',
|
||||
optionId: ToolConfirmationOutcome.ProceedOnce,
|
||||
},
|
||||
});
|
||||
|
||||
const stream1 = createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: {
|
||||
functionCalls: [{ name: 'test_tool', args: {} }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const stream2 = createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: { candidates: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
mockChat.sendMessageStream
|
||||
.mockResolvedValueOnce(stream1)
|
||||
.mockResolvedValueOnce(stream2);
|
||||
|
||||
await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [{ type: 'text', text: 'Call tool' }],
|
||||
});
|
||||
|
||||
expect(mockConnection.requestPermission).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
optionId: ToolConfirmationOutcome.ProceedAlways,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use filePath for ACP diff content in permission request', async () => {
|
||||
const confirmationDetails = {
|
||||
type: 'edit',
|
||||
|
||||
@@ -908,7 +908,7 @@ export class Session {
|
||||
|
||||
const params: acp.RequestPermissionRequest = {
|
||||
sessionId: this.id,
|
||||
options: toPermissionOptions(confirmationDetails, this.config),
|
||||
options: toPermissionOptions(confirmationDetails),
|
||||
toolCall: {
|
||||
toolCallId: callId,
|
||||
status: 'pending',
|
||||
@@ -1457,76 +1457,60 @@ const basicPermissionOptions = [
|
||||
|
||||
function toPermissionOptions(
|
||||
confirmation: ToolCallConfirmationDetails,
|
||||
config: Config,
|
||||
): acp.PermissionOption[] {
|
||||
const disableAlwaysAllow = config.getDisableAlwaysAllow();
|
||||
const options: acp.PermissionOption[] = [];
|
||||
|
||||
if (!disableAlwaysAllow) {
|
||||
switch (confirmation.type) {
|
||||
case 'edit':
|
||||
options.push({
|
||||
switch (confirmation.type) {
|
||||
case 'edit':
|
||||
return [
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.ProceedAlways,
|
||||
name: 'Allow All Edits',
|
||||
kind: 'allow_always',
|
||||
});
|
||||
break;
|
||||
case 'exec':
|
||||
options.push({
|
||||
},
|
||||
...basicPermissionOptions,
|
||||
];
|
||||
case 'exec':
|
||||
return [
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.ProceedAlways,
|
||||
name: `Always Allow ${confirmation.rootCommand}`,
|
||||
kind: 'allow_always',
|
||||
});
|
||||
break;
|
||||
case 'mcp':
|
||||
options.push(
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.ProceedAlwaysServer,
|
||||
name: `Always Allow ${confirmation.serverName}`,
|
||||
kind: 'allow_always',
|
||||
},
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.ProceedAlwaysTool,
|
||||
name: `Always Allow ${confirmation.toolName}`,
|
||||
kind: 'allow_always',
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'info':
|
||||
options.push({
|
||||
},
|
||||
...basicPermissionOptions,
|
||||
];
|
||||
case 'mcp':
|
||||
return [
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.ProceedAlwaysServer,
|
||||
name: `Always Allow ${confirmation.serverName}`,
|
||||
kind: 'allow_always',
|
||||
},
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.ProceedAlwaysTool,
|
||||
name: `Always Allow ${confirmation.toolName}`,
|
||||
kind: 'allow_always',
|
||||
},
|
||||
...basicPermissionOptions,
|
||||
];
|
||||
case 'info':
|
||||
return [
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.ProceedAlways,
|
||||
name: `Always Allow`,
|
||||
kind: 'allow_always',
|
||||
});
|
||||
break;
|
||||
case 'ask_user':
|
||||
case 'exit_plan_mode':
|
||||
// askuser and exit_plan_mode don't need "always allow" options
|
||||
break;
|
||||
default:
|
||||
// No "always allow" options for other types
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
options.push(...basicPermissionOptions);
|
||||
|
||||
// Exhaustive check
|
||||
switch (confirmation.type) {
|
||||
case 'edit':
|
||||
case 'exec':
|
||||
case 'mcp':
|
||||
case 'info':
|
||||
},
|
||||
...basicPermissionOptions,
|
||||
];
|
||||
case 'ask_user':
|
||||
// askuser doesn't need "always allow" options since it's asking questions
|
||||
return [...basicPermissionOptions];
|
||||
case 'exit_plan_mode':
|
||||
break;
|
||||
// exit_plan_mode doesn't need "always allow" options since it's a plan approval flow
|
||||
return [...basicPermissionOptions];
|
||||
default: {
|
||||
const unreachable: never = confirmation;
|
||||
throw new Error(`Unexpected: ${unreachable}`);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,16 +4,13 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
listExtensions,
|
||||
type Config,
|
||||
getErrorMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { listExtensions, type Config } from '@google/gemini-cli-core';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import {
|
||||
ExtensionManager,
|
||||
inferInstallMetadata,
|
||||
} from '../../config/extension-manager.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { McpServerEnablementManager } from '../../config/mcp/mcpServerEnablement.js';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import type {
|
||||
|
||||
@@ -105,7 +105,6 @@ export class AddMemoryCommand implements Command {
|
||||
|
||||
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
|
||||
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
|
||||
sandboxManager: context.config.sandboxManager,
|
||||
});
|
||||
await refreshMemory(context.config);
|
||||
return {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
SettingScope,
|
||||
type LoadedSettings,
|
||||
} from '../../config/settings.js';
|
||||
import { getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
// Mock dependencies
|
||||
const emitConsoleLog = vi.hoisted(() => vi.fn());
|
||||
@@ -44,12 +44,12 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
emitConsoleLog,
|
||||
},
|
||||
debugLogger,
|
||||
getErrorMessage: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../config/extension-manager.js');
|
||||
vi.mock('../../config/settings.js');
|
||||
vi.mock('../../utils/errors.js');
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
requestConsentNonInteractive: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
debugLogger,
|
||||
FolderTrustDiscoveryService,
|
||||
getRealPath,
|
||||
getErrorMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import {
|
||||
INSTALL_WARNING_MESSAGE,
|
||||
promptForConsentNonInteractive,
|
||||
|
||||
@@ -13,24 +13,26 @@ import {
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { coreEvents, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
import { type Argv } from 'yargs';
|
||||
import { handleLink, linkCommand } from './link.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const { mockCoreDebugLogger } = await import(
|
||||
'../../test-utils/mockDebugLogger.js'
|
||||
);
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
const mocked = mockCoreDebugLogger(actual, { stripAnsi: true });
|
||||
return { ...mocked, getErrorMessage: vi.fn() };
|
||||
return mockCoreDebugLogger(
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>(),
|
||||
{ stripAnsi: true },
|
||||
);
|
||||
});
|
||||
|
||||
vi.mock('../../config/extension-manager.js');
|
||||
vi.mock('../../config/settings.js');
|
||||
vi.mock('../../utils/errors.js');
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
requestConsentNonInteractive: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -8,10 +8,10 @@ import type { CommandModule } from 'yargs';
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
debugLogger,
|
||||
getErrorMessage,
|
||||
type ExtensionInstallMetadata,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import {
|
||||
INSTALL_WARNING_MESSAGE,
|
||||
requestConsentNonInteractive,
|
||||
|
||||
@@ -5,23 +5,27 @@
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { coreEvents, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
import { handleList, listCommand } from './list.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const { mockCoreDebugLogger } = await import(
|
||||
'../../test-utils/mockDebugLogger.js'
|
||||
);
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
const mocked = mockCoreDebugLogger(actual, { stripAnsi: false });
|
||||
return { ...mocked, getErrorMessage: vi.fn() };
|
||||
return mockCoreDebugLogger(
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>(),
|
||||
{
|
||||
stripAnsi: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
vi.mock('../../config/extension-manager.js');
|
||||
vi.mock('../../config/settings.js');
|
||||
vi.mock('../../utils/errors.js');
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
requestConsentNonInteractive: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
|
||||
@@ -18,7 +18,7 @@ import { type Argv } from 'yargs';
|
||||
import { handleUninstall, uninstallCommand } from './uninstall.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
import { getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
// NOTE: This file uses vi.hoisted() mocks to enable testing of sequential
|
||||
// mock behaviors (mockResolvedValueOnce/mockRejectedValueOnce chaining).
|
||||
@@ -66,11 +66,11 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
emitConsoleLog,
|
||||
},
|
||||
debugLogger,
|
||||
getErrorMessage: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../config/settings.js');
|
||||
vi.mock('../../utils/errors.js');
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
requestConsentNonInteractive: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
|
||||
@@ -12,12 +12,9 @@ import {
|
||||
updateExtension,
|
||||
} from '../../config/extensions/update.js';
|
||||
import { checkForExtensionUpdate } from '../../config/extensions/github.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
|
||||
import {
|
||||
coreEvents,
|
||||
debugLogger,
|
||||
getErrorMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { coreEvents, debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import semver from 'semver';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import type { ExtensionConfig } from '../../config/extension.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
|
||||
@@ -28,9 +28,6 @@ const { debugLogger, emitConsoleLog } = await vi.hoisted(async () => {
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
debugLogger,
|
||||
getErrorMessage: vi.fn((e: unknown) =>
|
||||
e instanceof Error ? e.message : String(e),
|
||||
),
|
||||
}));
|
||||
|
||||
import { handleInstall, installCommand } from './install.js';
|
||||
|
||||
@@ -5,11 +5,8 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
debugLogger,
|
||||
type SkillDefinition,
|
||||
getErrorMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { debugLogger, type SkillDefinition } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { installSkill } from '../../utils/skillUtils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -24,9 +24,6 @@ const { debugLogger } = await vi.hoisted(async () => {
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
debugLogger,
|
||||
getErrorMessage: vi.fn((e: unknown) =>
|
||||
e instanceof Error ? e.message : String(e),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import {
|
||||
requestConsentNonInteractive,
|
||||
|
||||
@@ -21,9 +21,6 @@ const { debugLogger, emitConsoleLog } = await vi.hoisted(async () => {
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
debugLogger,
|
||||
getErrorMessage: vi.fn((e: unknown) =>
|
||||
e instanceof Error ? e.message : String(e),
|
||||
),
|
||||
}));
|
||||
|
||||
import { handleUninstall, uninstallCommand } from './uninstall.js';
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { uninstallSkill } from '../../utils/skillUtils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -1773,7 +1773,7 @@ describe('loadCliConfig model selection', () => {
|
||||
});
|
||||
|
||||
it('always prefers model from argv', async () => {
|
||||
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash-preview'];
|
||||
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings({
|
||||
@@ -1785,11 +1785,11 @@ describe('loadCliConfig model selection', () => {
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(config.getModel()).toBe('gemini-2.5-flash-preview');
|
||||
expect(config.getModel()).toBe('gemini-2.5-flash');
|
||||
});
|
||||
|
||||
it('selects the model from argv if provided', async () => {
|
||||
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash-preview'];
|
||||
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings({
|
||||
@@ -1799,7 +1799,7 @@ describe('loadCliConfig model selection', () => {
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(config.getModel()).toBe('gemini-2.5-flash-preview');
|
||||
expect(config.getModel()).toBe('gemini-2.5-flash');
|
||||
});
|
||||
|
||||
it('selects the default auto model if provided via auto alias', async () => {
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
type HierarchicalMemory,
|
||||
coreEvents,
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
isValidModelOrAlias,
|
||||
getValidModelsAndAliases,
|
||||
getAdminErrorMessage,
|
||||
isHeadlessMode,
|
||||
Config,
|
||||
@@ -671,6 +673,18 @@ export async function loadCliConfig(
|
||||
const specifiedModel =
|
||||
argv.model || process.env['GEMINI_MODEL'] || settings.model?.name;
|
||||
|
||||
// Validate the model if one was explicitly specified
|
||||
if (specifiedModel && specifiedModel !== GEMINI_MODEL_ALIAS_AUTO) {
|
||||
if (!isValidModelOrAlias(specifiedModel)) {
|
||||
const validModels = getValidModelsAndAliases();
|
||||
|
||||
throw new FatalConfigError(
|
||||
`Invalid model: "${specifiedModel}"\n\n` +
|
||||
`Valid models and aliases:\n${validModels.map((m) => ` - ${m}`).join('\n')}\n\n` +
|
||||
`Use /model to switch models interactively.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const resolvedModel =
|
||||
specifiedModel === GEMINI_MODEL_ALIAS_AUTO
|
||||
? defaultModel
|
||||
@@ -730,7 +744,6 @@ export async function loadCliConfig(
|
||||
clientVersion: await getVersion(),
|
||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
sandbox: sandboxConfig,
|
||||
toolSandboxing: settings.security?.toolSandboxing ?? false,
|
||||
targetDir: cwd,
|
||||
includeDirectoryTree,
|
||||
includeDirectories,
|
||||
@@ -771,9 +784,6 @@ export async function loadCliConfig(
|
||||
approvalMode,
|
||||
disableYoloMode:
|
||||
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
|
||||
disableAlwaysAllow:
|
||||
settings.security?.disableAlwaysAllow ||
|
||||
settings.admin?.secureModeEnabled,
|
||||
showMemoryUsage: settings.ui?.showMemoryUsage || false,
|
||||
accessibility: {
|
||||
...settings.ui?.accessibility,
|
||||
@@ -813,7 +823,6 @@ export async function loadCliConfig(
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
|
||||
toolOutputMasking: settings.experimental?.toolOutputMasking,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
@@ -848,6 +857,7 @@ export async function loadCliConfig(
|
||||
disableLLMCorrection: settings.tools?.disableLLMCorrection,
|
||||
rawOutput: argv.rawOutput,
|
||||
acceptRawOutputRisk: argv.acceptRawOutputRisk,
|
||||
dynamicModelConfiguration: settings.experimental?.dynamicModelConfiguration,
|
||||
modelConfigServiceConfig: settings.modelConfigs,
|
||||
// TODO: loading of hooks based on workspace trust
|
||||
enableHooks: settings.hooksConfig.enabled,
|
||||
|
||||
@@ -20,12 +20,7 @@ import {
|
||||
import { createExtension } from '../test-utils/createExtension.js';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
|
||||
import {
|
||||
GEMINI_DIR,
|
||||
type Config,
|
||||
tmpdir,
|
||||
NoopSandboxManager,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { GEMINI_DIR, type Config, tmpdir } from '@google/gemini-cli-core';
|
||||
import { createTestMergedSettings, SettingScope } from './settings.js';
|
||||
|
||||
describe('ExtensionManager theme loading', () => {
|
||||
@@ -122,7 +117,6 @@ describe('ExtensionManager theme loading', () => {
|
||||
terminalHeight: 24,
|
||||
showColor: false,
|
||||
pager: 'cat',
|
||||
sandboxManager: new NoopSandboxManager(),
|
||||
sanitizationConfig: {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
*/
|
||||
|
||||
import { simpleGit } from 'simple-git';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import {
|
||||
debugLogger,
|
||||
getErrorMessage,
|
||||
type ExtensionInstallMetadata,
|
||||
type GeminiCLIExtension,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
@@ -11,12 +11,9 @@ import {
|
||||
} from '../../ui/state/extensions.js';
|
||||
import { loadInstallMetadata } from '../extension.js';
|
||||
import { checkForExtensionUpdate } from './github.js';
|
||||
import {
|
||||
debugLogger,
|
||||
getErrorMessage,
|
||||
type GeminiCLIExtension,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { debugLogger, type GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { copyExtension, type ExtensionManager } from '../extension-manager.js';
|
||||
import { ExtensionStorage } from './storage.js';
|
||||
|
||||
|
||||
@@ -63,9 +63,6 @@ export async function createPolicyEngineConfig(
|
||||
policyPaths: settings.policyPaths,
|
||||
adminPolicyPaths: settings.adminPolicyPaths,
|
||||
workspacePoliciesDir,
|
||||
disableAlwaysAllow:
|
||||
settings.security?.disableAlwaysAllow ||
|
||||
settings.admin?.secureModeEnabled,
|
||||
};
|
||||
|
||||
return createCorePolicyEngineConfig(policySettings, approvalMode);
|
||||
|
||||
@@ -34,9 +34,7 @@ const VALID_SANDBOX_COMMANDS = [
|
||||
function isSandboxCommand(
|
||||
value: string,
|
||||
): value is Exclude<SandboxConfig['command'], undefined> {
|
||||
return (VALID_SANDBOX_COMMANDS as ReadonlyArray<string | undefined>).includes(
|
||||
value,
|
||||
);
|
||||
return VALID_SANDBOX_COMMANDS.includes(value);
|
||||
}
|
||||
|
||||
function getSandboxCommand(
|
||||
|
||||
@@ -524,19 +524,16 @@ describe('Settings Loading and Merging', () => {
|
||||
const userSettingsContent = {
|
||||
security: {
|
||||
disableYoloMode: false,
|
||||
disableAlwaysAllow: false,
|
||||
},
|
||||
};
|
||||
const workspaceSettingsContent = {
|
||||
security: {
|
||||
disableYoloMode: false, // This should be ignored
|
||||
disableAlwaysAllow: false, // This should be ignored
|
||||
},
|
||||
};
|
||||
const systemSettingsContent = {
|
||||
security: {
|
||||
disableYoloMode: true,
|
||||
disableAlwaysAllow: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -554,7 +551,6 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.security?.disableYoloMode).toBe(true); // System setting should be used
|
||||
expect(settings.merged.security?.disableAlwaysAllow).toBe(true); // System setting should be used
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -2598,7 +2594,7 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Failed to save settings: Write failed',
|
||||
'There was an error saving your latest settings changes.',
|
||||
error,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
FatalConfigError,
|
||||
GEMINI_DIR,
|
||||
getErrorMessage,
|
||||
getFsErrorMessage,
|
||||
Storage,
|
||||
coreEvents,
|
||||
homedir,
|
||||
@@ -1073,10 +1072,9 @@ export function saveSettings(settingsFile: SettingsFile): void {
|
||||
settingsToSave as Record<string, unknown>,
|
||||
);
|
||||
} catch (error) {
|
||||
const detailedErrorMessage = getFsErrorMessage(error);
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Failed to save settings: ${detailedErrorMessage}`,
|
||||
'There was an error saving your latest settings changes.',
|
||||
error,
|
||||
);
|
||||
}
|
||||
@@ -1089,10 +1087,9 @@ export function saveModelChange(
|
||||
try {
|
||||
loadedSettings.setValue(SettingScope.User, 'model.name', model);
|
||||
} catch (error) {
|
||||
const detailedErrorMessage = getFsErrorMessage(error);
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Failed to save preferred model: ${detailedErrorMessage}`,
|
||||
'There was an error saving your preferred model.',
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1039,6 +1039,20 @@ const SETTINGS_SCHEMA = {
|
||||
'Apply specific configuration overrides based on matches, with a primary key of model (or alias). The most specific match will be used.',
|
||||
showInDialog: false,
|
||||
},
|
||||
modelDefinitions: {
|
||||
type: 'object',
|
||||
label: 'Model Definitions',
|
||||
category: 'Model',
|
||||
requiresRestart: true,
|
||||
default: DEFAULT_MODEL_CONFIGS.modelDefinitions,
|
||||
description:
|
||||
'Registry of model metadata, including tier, family, and features.',
|
||||
showInDialog: false,
|
||||
additionalProperties: {
|
||||
type: 'object',
|
||||
ref: 'ModelDefinition',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1117,19 +1131,6 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Model override for the visual agent.',
|
||||
showInDialog: false,
|
||||
},
|
||||
allowedDomains: {
|
||||
type: 'array',
|
||||
label: 'Allowed Domains',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: ['github.com', '*.google.com', 'localhost'] as string[],
|
||||
description: oneLine`
|
||||
A list of allowed domains for the browser agent
|
||||
(e.g., ["github.com", "*.google.com"]).
|
||||
`,
|
||||
showInDialog: false,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
disableUserInput: {
|
||||
type: 'boolean',
|
||||
label: 'Disable User Input',
|
||||
@@ -1300,7 +1301,7 @@ const SETTINGS_SCHEMA = {
|
||||
default: undefined as boolean | string | SandboxConfig | undefined,
|
||||
ref: 'BooleanOrStringOrObject',
|
||||
description: oneLine`
|
||||
Legacy full-process sandbox execution environment.
|
||||
Sandbox execution environment.
|
||||
Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile,
|
||||
or specify an explicit sandbox command (e.g., "docker", "podman", "lxc").
|
||||
`,
|
||||
@@ -1522,16 +1523,6 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Security-related settings.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
toolSandboxing: {
|
||||
type: 'boolean',
|
||||
label: 'Tool Sandboxing',
|
||||
category: 'Security',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Experimental tool-level sandboxing (implementation in progress).',
|
||||
showInDialog: true,
|
||||
},
|
||||
disableYoloMode: {
|
||||
type: 'boolean',
|
||||
label: 'Disable YOLO Mode',
|
||||
@@ -1541,16 +1532,6 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Disable YOLO mode, even if enabled by a flag.',
|
||||
showInDialog: true,
|
||||
},
|
||||
disableAlwaysAllow: {
|
||||
type: 'boolean',
|
||||
label: 'Disable Always Allow',
|
||||
category: 'Security',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Disable "Always allow" options in tool confirmation dialogs.',
|
||||
showInDialog: true,
|
||||
},
|
||||
enablePermanentToolApproval: {
|
||||
type: 'boolean',
|
||||
label: 'Allow Permanent Tool Approval',
|
||||
@@ -1943,6 +1924,16 @@ const SETTINGS_SCHEMA = {
|
||||
'Enable web fetch behavior that bypasses LLM summarization.',
|
||||
showInDialog: true,
|
||||
},
|
||||
dynamicModelConfiguration: {
|
||||
type: 'boolean',
|
||||
label: 'Dynamic Model Configuration',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable dynamic model configuration (definitions, resolutions, and chains) via settings.',
|
||||
showInDialog: false,
|
||||
},
|
||||
gemmaModelRouter: {
|
||||
type: 'object',
|
||||
label: 'Gemma Model Router',
|
||||
@@ -1994,18 +1985,9 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
topicUpdateNarration: {
|
||||
type: 'boolean',
|
||||
label: 'Topic & Update Narration',
|
||||
category: 'Experimental',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
extensions: {
|
||||
type: 'object',
|
||||
label: 'Extensions',
|
||||
@@ -2286,8 +2268,7 @@ const SETTINGS_SCHEMA = {
|
||||
category: 'Admin',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'If true, disallows YOLO mode and "Always allow" options from being used.',
|
||||
description: 'If true, disallows yolo mode from being used.',
|
||||
showInDialog: false,
|
||||
mergeStrategy: MergeStrategy.REPLACE,
|
||||
},
|
||||
@@ -2769,6 +2750,25 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
|
||||
},
|
||||
},
|
||||
},
|
||||
ModelDefinition: {
|
||||
type: 'object',
|
||||
description: 'Model metadata registry entry.',
|
||||
properties: {
|
||||
displayName: { type: 'string' },
|
||||
tier: { enum: ['pro', 'flash', 'flash-lite', 'custom', 'auto'] },
|
||||
family: { type: 'string' },
|
||||
isPreview: { type: 'boolean' },
|
||||
dialogLocation: { enum: ['main', 'manual'] },
|
||||
dialogDescription: { type: 'string' },
|
||||
features: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
thinking: { type: 'boolean' },
|
||||
multimodalToolUse: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getSettingsSchema(): SettingsSchemaType {
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ApprovalMode,
|
||||
getShellConfiguration,
|
||||
PolicyDecision,
|
||||
NoopSandboxManager,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { quote } from 'shell-quote';
|
||||
import { createPartFromText } from '@google/genai';
|
||||
@@ -78,14 +77,7 @@ describe('ShellProcessor', () => {
|
||||
getTargetDir: vi.fn().mockReturnValue('/test/dir'),
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getEnableInteractiveShell: vi.fn().mockReturnValue(false),
|
||||
getShellExecutionConfig: vi.fn().mockReturnValue({
|
||||
sandboxManager: new NoopSandboxManager(),
|
||||
sanitizationConfig: {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
},
|
||||
}),
|
||||
getShellExecutionConfig: vi.fn().mockReturnValue({}),
|
||||
getPolicyEngine: vi.fn().mockReturnValue({
|
||||
check: mockPolicyEngineCheck,
|
||||
}),
|
||||
|
||||
@@ -487,7 +487,7 @@ export class AppRig {
|
||||
}
|
||||
|
||||
async waitForPendingConfirmation(
|
||||
toolNameOrDisplayName?: string | RegExp | string[],
|
||||
toolNameOrDisplayName?: string | RegExp,
|
||||
timeout = 30000,
|
||||
): Promise<PendingConfirmation> {
|
||||
const matches = (p: PendingConfirmation) => {
|
||||
@@ -498,12 +498,6 @@ export class AppRig {
|
||||
p.toolDisplayName === toolNameOrDisplayName
|
||||
);
|
||||
}
|
||||
if (Array.isArray(toolNameOrDisplayName)) {
|
||||
return (
|
||||
toolNameOrDisplayName.includes(p.toolName) ||
|
||||
toolNameOrDisplayName.includes(p.toolDisplayName || '')
|
||||
);
|
||||
}
|
||||
return (
|
||||
toolNameOrDisplayName.test(p.toolName) ||
|
||||
toolNameOrDisplayName.test(p.toolDisplayName || '')
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I am going to read the secret file."},{"functionCall":{"name":"read_file","args":{"file_path":"secret.txt"}}}]},"finishReason":"STOP"}]}]}
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
import { NoopSandboxManager } from '@google/gemini-cli-core';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import {
|
||||
createTestMergedSettings,
|
||||
@@ -122,7 +121,6 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getBannerTextNoCapacityIssues: vi.fn().mockResolvedValue(''),
|
||||
getBannerTextCapacityIssues: vi.fn().mockResolvedValue(''),
|
||||
isInteractiveShellEnabled: vi.fn().mockReturnValue(false),
|
||||
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
|
||||
reloadSkills: vi.fn().mockResolvedValue(undefined),
|
||||
reloadAgents: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -133,14 +131,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getRetryFetchErrors: vi.fn().mockReturnValue(true),
|
||||
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
||||
getShellToolInactivityTimeout: vi.fn().mockReturnValue(300000),
|
||||
getShellExecutionConfig: vi.fn().mockReturnValue({
|
||||
sandboxManager: new NoopSandboxManager(),
|
||||
sanitizationConfig: {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
},
|
||||
}),
|
||||
getShellExecutionConfig: vi.fn().mockReturnValue({}),
|
||||
setShellExecutionConfig: vi.fn(),
|
||||
getEnableToolOutputTruncation: vi.fn().mockReturnValue(true),
|
||||
getTruncateToolOutputThreshold: vi.fn().mockReturnValue(1000),
|
||||
|
||||
@@ -1425,7 +1425,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
pager: settings.merged.tools.shell.pager,
|
||||
showColor: settings.merged.tools.shell.showColor,
|
||||
sanitizationConfig: config.sanitizationConfig,
|
||||
sandboxManager: config.sandboxManager,
|
||||
});
|
||||
|
||||
const { isFocused, hasReceivedFocusEvent } = useFocus();
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { AppRig } from '../test-utils/AppRig.js';
|
||||
import { PolicyDecision } from '@google/gemini-cli-core';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe('Policy Engine Visual Validation', () => {
|
||||
let rig: AppRig;
|
||||
|
||||
beforeEach(async () => {
|
||||
const fakeResponsesPath = path.join(
|
||||
__dirname,
|
||||
'../test-utils/fixtures/policy-test.responses',
|
||||
);
|
||||
rig = new AppRig({
|
||||
fakeResponsesPath,
|
||||
});
|
||||
await rig.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rig.unmount();
|
||||
});
|
||||
|
||||
it('should boot correctly and display the main interface', async () => {
|
||||
rig.render();
|
||||
await rig.waitForIdle();
|
||||
expect(rig.lastFrame).toContain('Type your message');
|
||||
});
|
||||
|
||||
it.todo(
|
||||
'should visually render a DENY decision when a tool is blocked',
|
||||
async () => {
|
||||
rig.setToolPolicy('read_file', PolicyDecision.DENY);
|
||||
rig.render();
|
||||
|
||||
await rig.sendMessage('Read secret.txt');
|
||||
|
||||
// Wait for the model's initial text response
|
||||
await rig.waitForOutput(/I am going to read the secret file/i);
|
||||
|
||||
// Wait for the blocked message to appear
|
||||
await rig.waitForOutput(/Blocked by policy/i);
|
||||
|
||||
// Verify it matches the SVG snapshot
|
||||
await expect(rig).toMatchSvgSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
it.todo(
|
||||
'should visually render an ASK_USER prompt for policy approval',
|
||||
async () => {
|
||||
rig.setToolPolicy('read_file', PolicyDecision.ASK_USER);
|
||||
rig.render();
|
||||
|
||||
await rig.sendMessage('Read secret.txt');
|
||||
|
||||
// Wait for the model's initial text response
|
||||
await rig.waitForOutput(/I am going to read the secret file/i);
|
||||
|
||||
// Wait for the confirmation prompt
|
||||
await rig.waitForOutput(/Allow execution/i);
|
||||
|
||||
// Verify it matches the SVG snapshot
|
||||
await expect(rig).toMatchSvgSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -7,10 +7,10 @@
|
||||
import {
|
||||
debugLogger,
|
||||
listExtensions,
|
||||
getErrorMessage,
|
||||
type ExtensionInstallMetadata,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { ExtensionUpdateInfo } from '../../config/extension.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import {
|
||||
emptyIcon,
|
||||
MessageType,
|
||||
|
||||
@@ -16,8 +16,9 @@ import {
|
||||
MessageType,
|
||||
} from '../types.js';
|
||||
import { disableSkill, enableSkill } from '../../utils/skillSettings.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
import { getAdminErrorMessage, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { getAdminErrorMessage } from '@google/gemini-cli-core';
|
||||
import {
|
||||
linkSkill,
|
||||
renderSkillActionFeedback,
|
||||
|
||||
@@ -116,9 +116,38 @@ const Kbd = ({ name, shortcut }: { name: string; shortcut: string }) => (
|
||||
</>
|
||||
);
|
||||
|
||||
import { SessionBrowserLoading } from './SessionBrowser/SessionBrowserLoading.js';
|
||||
import { SessionBrowserError } from './SessionBrowser/SessionBrowserError.js';
|
||||
import { SessionBrowserEmpty } from './SessionBrowser/SessionBrowserEmpty.js';
|
||||
/**
|
||||
* Loading state component displayed while sessions are being loaded.
|
||||
*/
|
||||
const SessionBrowserLoading = (): React.JSX.Element => (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Text color={Colors.Gray}>Loading sessions…</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* Error state component displayed when session loading fails.
|
||||
*/
|
||||
const SessionBrowserError = ({
|
||||
state,
|
||||
}: {
|
||||
state: SessionBrowserState;
|
||||
}): React.JSX.Element => (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Text color={Colors.AccentRed}>Error: {state.error}</Text>
|
||||
<Text color={Colors.Gray}>Press q to exit</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* Empty state component displayed when no sessions are found.
|
||||
*/
|
||||
const SessionBrowserEmpty = (): React.JSX.Element => (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Text color={Colors.Gray}>No auto-saved conversations found.</Text>
|
||||
<Text color={Colors.Gray}>Press q to exit</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
import { sortSessions, filterSessions } from './SessionBrowser/utils.js';
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../../colors.js';
|
||||
|
||||
/**
|
||||
* Empty state component displayed when no sessions are found.
|
||||
*/
|
||||
export const SessionBrowserEmpty = (): React.JSX.Element => (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Text color={Colors.Gray}>No auto-saved conversations found.</Text>
|
||||
<Text color={Colors.Gray}>Press q to exit</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../../colors.js';
|
||||
import type { SessionBrowserState } from '../SessionBrowser.js';
|
||||
|
||||
/**
|
||||
* Error state component displayed when session loading fails.
|
||||
*/
|
||||
export const SessionBrowserError = ({
|
||||
state,
|
||||
}: {
|
||||
state: SessionBrowserState;
|
||||
}): React.JSX.Element => (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Text color={Colors.AccentRed}>Error: {state.error}</Text>
|
||||
<Text color={Colors.Gray}>Press q to exit</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../../colors.js';
|
||||
|
||||
/**
|
||||
* Loading state component displayed while sessions are being loaded.
|
||||
*/
|
||||
export const SessionBrowserLoading = (): React.JSX.Element => (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Text color={Colors.Gray}>Loading sessions…</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SessionBrowserLoading } from './SessionBrowserLoading.js';
|
||||
import { SessionBrowserError } from './SessionBrowserError.js';
|
||||
import { SessionBrowserEmpty } from './SessionBrowserEmpty.js';
|
||||
import type { SessionBrowserState } from '../SessionBrowser.js';
|
||||
|
||||
describe('SessionBrowser UI States', () => {
|
||||
it('SessionBrowserLoading renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(<SessionBrowserLoading />);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('SessionBrowserError renders correctly', async () => {
|
||||
const mockState = { error: 'Test error message' } as SessionBrowserState;
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<SessionBrowserError state={mockState} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('SessionBrowserEmpty renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(<SessionBrowserEmpty />);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`SessionBrowser UI States > SessionBrowserEmpty renders correctly 1`] = `
|
||||
" No auto-saved conversations found.
|
||||
Press q to exit
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`SessionBrowser UI States > SessionBrowserError renders correctly 1`] = `
|
||||
" Error: Test error message
|
||||
Press q to exit
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`SessionBrowser UI States > SessionBrowserLoading renders correctly 1`] = `
|
||||
" Loading sessions…
|
||||
"
|
||||
`;
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from '../utils/displayUtils.js';
|
||||
import { computeSessionStats } from '../utils/computeStats.js';
|
||||
import {
|
||||
type Config,
|
||||
type RetrieveUserQuotaResponse,
|
||||
isActiveModel,
|
||||
getDisplayString,
|
||||
@@ -88,13 +89,16 @@ const Section: React.FC<SectionProps> = ({ title, children }) => (
|
||||
// Logic for building the unified list of table rows
|
||||
const buildModelRows = (
|
||||
models: Record<string, ModelMetrics>,
|
||||
config: Config,
|
||||
quotas?: RetrieveUserQuotaResponse,
|
||||
useGemini3_1 = false,
|
||||
useCustomToolModel = false,
|
||||
) => {
|
||||
const getBaseModelName = (name: string) => name.replace('-001', '');
|
||||
const usedModelNames = new Set(
|
||||
Object.keys(models).map(getBaseModelName).map(getDisplayString),
|
||||
Object.keys(models)
|
||||
.map(getBaseModelName)
|
||||
.map((name) => getDisplayString(name, config)),
|
||||
);
|
||||
|
||||
// 1. Models with active usage
|
||||
@@ -104,7 +108,7 @@ const buildModelRows = (
|
||||
const inputTokens = metrics.tokens.input;
|
||||
return {
|
||||
key: name,
|
||||
modelName: getDisplayString(modelName),
|
||||
modelName: getDisplayString(modelName, config),
|
||||
requests: metrics.api.totalRequests,
|
||||
cachedTokens: cachedTokens.toLocaleString(),
|
||||
inputTokens: inputTokens.toLocaleString(),
|
||||
@@ -121,11 +125,11 @@ const buildModelRows = (
|
||||
(b) =>
|
||||
b.modelId &&
|
||||
isActiveModel(b.modelId, useGemini3_1, useCustomToolModel) &&
|
||||
!usedModelNames.has(getDisplayString(b.modelId)),
|
||||
!usedModelNames.has(getDisplayString(b.modelId, config)),
|
||||
)
|
||||
.map((bucket) => ({
|
||||
key: bucket.modelId!,
|
||||
modelName: getDisplayString(bucket.modelId!),
|
||||
modelName: getDisplayString(bucket.modelId!, config),
|
||||
requests: '-',
|
||||
cachedTokens: '-',
|
||||
inputTokens: '-',
|
||||
@@ -139,6 +143,7 @@ const buildModelRows = (
|
||||
|
||||
const ModelUsageTable: React.FC<{
|
||||
models: Record<string, ModelMetrics>;
|
||||
config: Config;
|
||||
quotas?: RetrieveUserQuotaResponse;
|
||||
cacheEfficiency: number;
|
||||
totalCachedTokens: number;
|
||||
@@ -150,6 +155,7 @@ const ModelUsageTable: React.FC<{
|
||||
useCustomToolModel?: boolean;
|
||||
}> = ({
|
||||
models,
|
||||
config,
|
||||
quotas,
|
||||
cacheEfficiency,
|
||||
totalCachedTokens,
|
||||
@@ -162,7 +168,13 @@ const ModelUsageTable: React.FC<{
|
||||
}) => {
|
||||
const { stdout } = useStdout();
|
||||
const terminalWidth = stdout?.columns ?? 84;
|
||||
const rows = buildModelRows(models, quotas, useGemini3_1, useCustomToolModel);
|
||||
const rows = buildModelRows(
|
||||
models,
|
||||
config,
|
||||
quotas,
|
||||
useGemini3_1,
|
||||
useCustomToolModel,
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
@@ -676,6 +688,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
</Section>
|
||||
<ModelUsageTable
|
||||
models={models}
|
||||
config={config}
|
||||
quotas={quotas}
|
||||
cacheEfficiency={computed.cacheEfficiency}
|
||||
totalCachedTokens={computed.totalCachedTokens}
|
||||
|
||||
@@ -42,7 +42,6 @@ describe('ToolConfirmationQueue', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
getModel: () => 'gemini-pro',
|
||||
getDebugMode: () => false,
|
||||
getTargetDir: () => '/mock/target/dir',
|
||||
|
||||
@@ -21,7 +21,6 @@ describe('ToolConfirmationMessage Redirection', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
it('should display redirection warning and tip for redirected commands', async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const TodoTray: React.FC = () => {
|
||||
const uiState = useUIState();
|
||||
|
||||
const todos: TodoList | null = useMemo(() => {
|
||||
// Find the most recent todo list written by tools that output a TodoList (e.g., WriteTodosTool or Tracker tools)
|
||||
// Find the most recent todo list written by the WriteTodosTool
|
||||
for (let i = uiState.history.length - 1; i >= 0; i--) {
|
||||
const entry = uiState.history[i];
|
||||
if (entry.type !== 'tool_group') {
|
||||
|
||||
@@ -37,7 +37,6 @@ describe('ToolConfirmationMessage', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
it('should not display urls if prompt and url are the same', async () => {
|
||||
@@ -332,8 +331,8 @@ describe('ToolConfirmationMessage', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
@@ -354,7 +353,6 @@ describe('ToolConfirmationMessage', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => false,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
@@ -390,8 +388,8 @@ describe('ToolConfirmationMessage', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
@@ -417,8 +415,8 @@ describe('ToolConfirmationMessage', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
callId="test-call-id"
|
||||
@@ -459,8 +457,8 @@ describe('ToolConfirmationMessage', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
vi.mocked(useToolActions).mockReturnValue({
|
||||
confirm: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
@@ -487,8 +485,8 @@ describe('ToolConfirmationMessage', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => true,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
vi.mocked(useToolActions).mockReturnValue({
|
||||
confirm: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
@@ -515,8 +513,8 @@ describe('ToolConfirmationMessage', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => true,
|
||||
getDisableAlwaysAllow: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
vi.mocked(useToolActions).mockReturnValue({
|
||||
confirm: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
|
||||
@@ -86,14 +86,12 @@ export const ToolConfirmationMessage: React.FC<
|
||||
|
||||
const settings = useSettings();
|
||||
const allowPermanentApproval =
|
||||
settings.merged.security.enablePermanentToolApproval &&
|
||||
!config.getDisableAlwaysAllow();
|
||||
settings.merged.security.enablePermanentToolApproval;
|
||||
|
||||
const handlesOwnUI =
|
||||
confirmationDetails.type === 'ask_user' ||
|
||||
confirmationDetails.type === 'exit_plan_mode';
|
||||
const isTrustedFolder =
|
||||
config.isTrustedFolder() && !config.getDisableAlwaysAllow();
|
||||
const isTrustedFolder = config.isTrustedFolder();
|
||||
|
||||
const handleConfirm = useCallback(
|
||||
(outcome: ToolConfirmationOutcome, payload?: ToolConfirmationPayload) => {
|
||||
|
||||
@@ -21,7 +21,6 @@ import { isShellTool } from './ToolShared.js';
|
||||
import {
|
||||
shouldHideToolCall,
|
||||
CoreToolCallStatus,
|
||||
ToolErrorType,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
|
||||
@@ -60,8 +59,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
if (
|
||||
isLowErrorVerbosity &&
|
||||
t.status === CoreToolCallStatus.Error &&
|
||||
!t.isClientInitiated &&
|
||||
t.errorType !== ToolErrorType.POLICY_VIOLATION
|
||||
!t.isClientInitiated
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -760,48 +760,6 @@ describe('BaseSettingsDialog', () => {
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should allow j and k characters to be typed in string edit fields without triggering navigation', async () => {
|
||||
const items = createMockItems(4);
|
||||
const stringItem = items.find((i) => i.type === 'string')!;
|
||||
const { stdin, waitUntilReady, unmount } = await renderDialog({
|
||||
items: [stringItem],
|
||||
});
|
||||
|
||||
// Enter edit mode
|
||||
await act(async () => {
|
||||
stdin.write(TerminalKeys.ENTER);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Type 'j' - should appear in field, NOT trigger navigation
|
||||
await act(async () => {
|
||||
stdin.write('j');
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Type 'k' - should appear in field, NOT trigger navigation
|
||||
await act(async () => {
|
||||
stdin.write('k');
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Commit with Enter
|
||||
await act(async () => {
|
||||
stdin.write(TerminalKeys.ENTER);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// j and k should be typed into the field
|
||||
await waitFor(() => {
|
||||
expect(mockOnEditCommit).toHaveBeenCalledWith(
|
||||
'string-setting',
|
||||
'test-valuejk', // entered value + j and k
|
||||
expect.objectContaining({ type: 'string' }),
|
||||
);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom key handling', () => {
|
||||
|
||||
@@ -325,18 +325,13 @@ export function BaseSettingsDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
// Up/Down in edit mode - commit and navigate.
|
||||
// Only trigger on non-insertable keys (arrow keys) so that typing
|
||||
// j/k characters into the edit buffer is not intercepted.
|
||||
if (keyMatchers[Command.DIALOG_NAVIGATION_UP](key) && !key.insertable) {
|
||||
// Up/Down in edit mode - commit and navigate
|
||||
if (keyMatchers[Command.DIALOG_NAVIGATION_UP](key)) {
|
||||
commitEdit();
|
||||
moveUp();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
keyMatchers[Command.DIALOG_NAVIGATION_DOWN](key) &&
|
||||
!key.insertable
|
||||
) {
|
||||
if (keyMatchers[Command.DIALOG_NAVIGATION_DOWN](key)) {
|
||||
commitEdit();
|
||||
moveDown();
|
||||
return;
|
||||
|
||||
@@ -647,15 +647,6 @@ describe('KeypressContext', () => {
|
||||
sequence: `\x1b[27;6;9~`,
|
||||
expected: { name: 'tab', shift: true, ctrl: true },
|
||||
},
|
||||
// Unicode CJK (Kitty/modifyOtherKeys scalar values)
|
||||
{
|
||||
sequence: '\x1b[44032u',
|
||||
expected: { name: '가', sequence: '가', insertable: true },
|
||||
},
|
||||
{
|
||||
sequence: '\x1b[27;1;44032~',
|
||||
expected: { name: '가', sequence: '가', insertable: true },
|
||||
},
|
||||
// XTerm Function Key
|
||||
{ sequence: `\x1b[1;129A`, expected: { name: 'up' } },
|
||||
{ sequence: `\x1b[1;2H`, expected: { name: 'home', shift: true } },
|
||||
@@ -1412,7 +1403,7 @@ describe('KeypressContext', () => {
|
||||
expect(keyHandler).toHaveBeenCalledTimes(inputString.length);
|
||||
for (const char of inputString) {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sequence: char, name: char.toLowerCase() }),
|
||||
expect.objectContaining({ sequence: char }),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -610,28 +610,20 @@ function* emitKeys(
|
||||
if (code.endsWith('u') || code.endsWith('~')) {
|
||||
// CSI-u or tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
|
||||
const codeNumber = parseInt(code.slice(1, -1), 10);
|
||||
const mapped = KITTY_CODE_MAP[codeNumber];
|
||||
if (mapped) {
|
||||
name = mapped.name;
|
||||
if (mapped.sequence && !ctrl && !cmd && !alt) {
|
||||
sequence = mapped.sequence;
|
||||
insertable = true;
|
||||
}
|
||||
} else if (
|
||||
codeNumber >= 33 && // Printable characters start after space (32),
|
||||
codeNumber <= 0x10ffff && // Valid Unicode scalar values (excluding control characters)
|
||||
(codeNumber < 0xd800 || codeNumber > 0xdfff) // Exclude UTF-16 surrogate halves
|
||||
) {
|
||||
// Valid printable Unicode scalar values (up to Unicode maximum)
|
||||
// Note: Kitty maps its special keys to the PUA (57344+), which are handled by KITTY_CODE_MAP above.
|
||||
const char = String.fromCodePoint(codeNumber);
|
||||
if (codeNumber >= 33 && codeNumber <= 126) {
|
||||
const char = String.fromCharCode(codeNumber);
|
||||
name = char.toLowerCase();
|
||||
if (char !== name) {
|
||||
if (char >= 'A' && char <= 'Z') {
|
||||
shift = true;
|
||||
}
|
||||
if (!ctrl && !cmd && !alt) {
|
||||
sequence = char;
|
||||
insertable = true;
|
||||
} else {
|
||||
const mapped = KITTY_CODE_MAP[codeNumber];
|
||||
if (mapped) {
|
||||
name = mapped.name;
|
||||
if (mapped.sequence && !ctrl && !cmd && !alt) {
|
||||
sequence = mapped.sequence;
|
||||
insertable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -704,10 +696,6 @@ function* emitKeys(
|
||||
alt = ch.length > 0;
|
||||
} else {
|
||||
// Any other character is considered printable.
|
||||
name = ch.toLowerCase();
|
||||
if (ch !== name) {
|
||||
shift = true;
|
||||
}
|
||||
insertable = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { NoopSandboxManager } from '@google/gemini-cli-core';
|
||||
|
||||
const mockIsBinary = vi.hoisted(() => vi.fn());
|
||||
const mockShellExecutionService = vi.hoisted(() => vi.fn());
|
||||
@@ -110,14 +109,8 @@ describe('useShellCommandProcessor', () => {
|
||||
getShellExecutionConfig: () => ({
|
||||
terminalHeight: 20,
|
||||
terminalWidth: 80,
|
||||
sandboxManager: new NoopSandboxManager(),
|
||||
sanitizationConfig: {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
},
|
||||
}),
|
||||
} as unknown as Config;
|
||||
} as Config;
|
||||
mockGeminiClient = { addHistory: vi.fn() } as unknown as GeminiClient;
|
||||
|
||||
vi.mocked(os.platform).mockReturnValue('linux');
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
type ToolResultDisplay,
|
||||
debugLogger,
|
||||
CoreToolCallStatus,
|
||||
type ToolErrorType,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
type HistoryItemToolGroup,
|
||||
@@ -64,7 +63,6 @@ export function mapToDisplay(
|
||||
let progressMessage: string | undefined = undefined;
|
||||
let progress: number | undefined = undefined;
|
||||
let progressTotal: number | undefined = undefined;
|
||||
let errorType: ToolErrorType | undefined = undefined;
|
||||
|
||||
switch (call.status) {
|
||||
case CoreToolCallStatus.Success:
|
||||
@@ -74,7 +72,6 @@ export function mapToDisplay(
|
||||
case CoreToolCallStatus.Error:
|
||||
case CoreToolCallStatus.Cancelled:
|
||||
resultDisplay = call.response.resultDisplay;
|
||||
errorType = call.response.errorType;
|
||||
break;
|
||||
case CoreToolCallStatus.AwaitingApproval:
|
||||
correlationId = call.correlationId;
|
||||
@@ -117,7 +114,6 @@ export function mapToDisplay(
|
||||
progressTotal,
|
||||
approvalMode: call.approvalMode,
|
||||
originalRequestName: call.request.originalRequestName,
|
||||
errorType,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
import {
|
||||
debugLogger,
|
||||
checkExhaustive,
|
||||
getErrorMessage,
|
||||
type GeminiCLIExtension,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import {
|
||||
ExtensionUpdateState,
|
||||
extensionUpdatesReducer,
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('KeyBinding', () => {
|
||||
describe('constructor', () => {
|
||||
it('should parse a simple key', () => {
|
||||
const binding = new KeyBinding('a');
|
||||
expect(binding.name).toBe('a');
|
||||
expect(binding.key).toBe('a');
|
||||
expect(binding.ctrl).toBe(false);
|
||||
expect(binding.shift).toBe(false);
|
||||
expect(binding.alt).toBe(false);
|
||||
@@ -31,45 +31,45 @@ describe('KeyBinding', () => {
|
||||
|
||||
it('should parse ctrl+key', () => {
|
||||
const binding = new KeyBinding('ctrl+c');
|
||||
expect(binding.name).toBe('c');
|
||||
expect(binding.key).toBe('c');
|
||||
expect(binding.ctrl).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse shift+key', () => {
|
||||
const binding = new KeyBinding('shift+z');
|
||||
expect(binding.name).toBe('z');
|
||||
expect(binding.key).toBe('z');
|
||||
expect(binding.shift).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse alt+key', () => {
|
||||
const binding = new KeyBinding('alt+left');
|
||||
expect(binding.name).toBe('left');
|
||||
expect(binding.key).toBe('left');
|
||||
expect(binding.alt).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse cmd+key', () => {
|
||||
const binding = new KeyBinding('cmd+f');
|
||||
expect(binding.name).toBe('f');
|
||||
expect(binding.key).toBe('f');
|
||||
expect(binding.cmd).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle aliases (option/opt/meta)', () => {
|
||||
const optionBinding = new KeyBinding('option+b');
|
||||
expect(optionBinding.name).toBe('b');
|
||||
expect(optionBinding.key).toBe('b');
|
||||
expect(optionBinding.alt).toBe(true);
|
||||
|
||||
const optBinding = new KeyBinding('opt+b');
|
||||
expect(optBinding.name).toBe('b');
|
||||
expect(optBinding.key).toBe('b');
|
||||
expect(optBinding.alt).toBe(true);
|
||||
|
||||
const metaBinding = new KeyBinding('meta+enter');
|
||||
expect(metaBinding.name).toBe('enter');
|
||||
expect(metaBinding.key).toBe('enter');
|
||||
expect(metaBinding.cmd).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse multiple modifiers', () => {
|
||||
const binding = new KeyBinding('ctrl+shift+alt+cmd+x');
|
||||
expect(binding.name).toBe('x');
|
||||
expect(binding.key).toBe('x');
|
||||
expect(binding.ctrl).toBe(true);
|
||||
expect(binding.shift).toBe(true);
|
||||
expect(binding.alt).toBe(true);
|
||||
@@ -78,14 +78,14 @@ describe('KeyBinding', () => {
|
||||
|
||||
it('should be case-insensitive', () => {
|
||||
const binding = new KeyBinding('CTRL+Shift+F');
|
||||
expect(binding.name).toBe('f');
|
||||
expect(binding.key).toBe('f');
|
||||
expect(binding.ctrl).toBe(true);
|
||||
expect(binding.shift).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle named keys with modifiers', () => {
|
||||
const binding = new KeyBinding('ctrl+enter');
|
||||
expect(binding.name).toBe('enter');
|
||||
expect(binding.key).toBe('enter');
|
||||
expect(binding.ctrl).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -144,14 +144,14 @@ export class KeyBinding {
|
||||
]);
|
||||
|
||||
/** The key name (e.g., 'a', 'enter', 'tab', 'escape') */
|
||||
readonly name: string;
|
||||
readonly key: string;
|
||||
readonly shift: boolean;
|
||||
readonly alt: boolean;
|
||||
readonly ctrl: boolean;
|
||||
readonly cmd: boolean;
|
||||
|
||||
constructor(pattern: string) {
|
||||
let remains = pattern.trim();
|
||||
let remains = pattern.toLowerCase().trim();
|
||||
let shift = false;
|
||||
let alt = false;
|
||||
let ctrl = false;
|
||||
@@ -160,32 +160,31 @@ export class KeyBinding {
|
||||
let matched: boolean;
|
||||
do {
|
||||
matched = false;
|
||||
const lowerRemains = remains.toLowerCase();
|
||||
if (lowerRemains.startsWith('ctrl+')) {
|
||||
if (remains.startsWith('ctrl+')) {
|
||||
ctrl = true;
|
||||
remains = remains.slice(5);
|
||||
matched = true;
|
||||
} else if (lowerRemains.startsWith('shift+')) {
|
||||
} else if (remains.startsWith('shift+')) {
|
||||
shift = true;
|
||||
remains = remains.slice(6);
|
||||
matched = true;
|
||||
} else if (lowerRemains.startsWith('alt+')) {
|
||||
} else if (remains.startsWith('alt+')) {
|
||||
alt = true;
|
||||
remains = remains.slice(4);
|
||||
matched = true;
|
||||
} else if (lowerRemains.startsWith('option+')) {
|
||||
} else if (remains.startsWith('option+')) {
|
||||
alt = true;
|
||||
remains = remains.slice(7);
|
||||
matched = true;
|
||||
} else if (lowerRemains.startsWith('opt+')) {
|
||||
} else if (remains.startsWith('opt+')) {
|
||||
alt = true;
|
||||
remains = remains.slice(4);
|
||||
matched = true;
|
||||
} else if (lowerRemains.startsWith('cmd+')) {
|
||||
} else if (remains.startsWith('cmd+')) {
|
||||
cmd = true;
|
||||
remains = remains.slice(4);
|
||||
matched = true;
|
||||
} else if (lowerRemains.startsWith('meta+')) {
|
||||
} else if (remains.startsWith('meta+')) {
|
||||
cmd = true;
|
||||
remains = remains.slice(5);
|
||||
matched = true;
|
||||
@@ -194,17 +193,15 @@ export class KeyBinding {
|
||||
|
||||
const key = remains;
|
||||
|
||||
const isSingleChar = [...key].length === 1;
|
||||
|
||||
if (!isSingleChar && !KeyBinding.VALID_LONG_KEYS.has(key.toLowerCase())) {
|
||||
if ([...key].length !== 1 && !KeyBinding.VALID_LONG_KEYS.has(key)) {
|
||||
throw new Error(
|
||||
`Invalid keybinding key: "${key}" in "${pattern}".` +
|
||||
` Must be a single character or one of: ${[...KeyBinding.VALID_LONG_KEYS].join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.name = key.toLowerCase();
|
||||
this.shift = shift || (isSingleChar && this.name !== key);
|
||||
this.key = key;
|
||||
this.shift = shift;
|
||||
this.alt = alt;
|
||||
this.ctrl = ctrl;
|
||||
this.cmd = cmd;
|
||||
@@ -212,7 +209,7 @@ export class KeyBinding {
|
||||
|
||||
matches(key: Key): boolean {
|
||||
return (
|
||||
key.name === this.name &&
|
||||
this.key === key.name &&
|
||||
!!key.shift === !!this.shift &&
|
||||
!!key.alt === !!this.alt &&
|
||||
!!key.ctrl === !!this.ctrl &&
|
||||
@@ -222,7 +219,7 @@ export class KeyBinding {
|
||||
|
||||
equals(other: KeyBinding): boolean {
|
||||
return (
|
||||
this.name === other.name &&
|
||||
this.key === other.key &&
|
||||
this.shift === other.shift &&
|
||||
this.alt === other.alt &&
|
||||
this.ctrl === other.ctrl &&
|
||||
|
||||
@@ -475,22 +475,6 @@ describe('keyMatchers', () => {
|
||||
expect(matchers[Command.QUIT](createKey('q', { ctrl: true }))).toBe(true);
|
||||
expect(matchers[Command.QUIT](createKey('q', { alt: true }))).toBe(true);
|
||||
});
|
||||
it('should support matching non-ASCII and CJK characters', () => {
|
||||
const config = new Map(defaultKeyBindingConfig);
|
||||
config.set(Command.QUIT, [new KeyBinding('Å'), new KeyBinding('가')]);
|
||||
|
||||
const matchers = createKeyMatchers(config);
|
||||
|
||||
// Å is normalized to å with shift=true by the parser
|
||||
expect(matchers[Command.QUIT](createKey('å', { shift: true }))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(matchers[Command.QUIT](createKey('å'))).toBe(false);
|
||||
|
||||
// CJK characters do not have a lower/upper case
|
||||
expect(matchers[Command.QUIT](createKey('가'))).toBe(true);
|
||||
expect(matchers[Command.QUIT](createKey('나'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
|
||||
@@ -86,7 +86,7 @@ export function formatKeyBinding(
|
||||
if (binding.shift) parts.push(modMap.shift);
|
||||
if (binding.cmd) parts.push(modMap.cmd);
|
||||
|
||||
const keyName = KEY_NAME_MAP[binding.name] || binding.name.toUpperCase();
|
||||
const keyName = KEY_NAME_MAP[binding.key] || binding.key.toUpperCase();
|
||||
parts.push(keyName);
|
||||
|
||||
return parts.join('+');
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
type AgentDefinition,
|
||||
type ApprovalMode,
|
||||
type Kind,
|
||||
type ToolErrorType,
|
||||
CoreToolCallStatus,
|
||||
checkExhaustive,
|
||||
} from '@google/gemini-cli-core';
|
||||
@@ -118,7 +117,6 @@ export interface IndividualToolCallDisplay {
|
||||
originalRequestName?: string;
|
||||
progress?: number;
|
||||
progressTotal?: number;
|
||||
errorType?: ToolErrorType;
|
||||
}
|
||||
|
||||
export interface CompressionProps {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
coreEvents,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
getErrorMessage,
|
||||
handleError,
|
||||
handleToolError,
|
||||
handleCancellationError,
|
||||
@@ -151,6 +152,25 @@ describe('errors', () => {
|
||||
processExitSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('getErrorMessage', () => {
|
||||
it('should return error message for Error instances', () => {
|
||||
const error = new Error('Test error message');
|
||||
expect(getErrorMessage(error)).toBe('Test error message');
|
||||
});
|
||||
|
||||
it('should convert non-Error values to strings', () => {
|
||||
expect(getErrorMessage('string error')).toBe('string error');
|
||||
expect(getErrorMessage(123)).toBe('123');
|
||||
expect(getErrorMessage(null)).toBe('null');
|
||||
expect(getErrorMessage(undefined)).toBe('undefined');
|
||||
});
|
||||
|
||||
it('should handle objects', () => {
|
||||
const obj = { message: 'test' };
|
||||
expect(getErrorMessage(obj)).toBe('[object Object]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleError', () => {
|
||||
describe('in text mode', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -18,10 +18,16 @@ import {
|
||||
isFatalToolError,
|
||||
debugLogger,
|
||||
coreEvents,
|
||||
getErrorMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { runSyncCleanup } from './cleanup.js';
|
||||
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
interface ErrorWithCode extends Error {
|
||||
exitCode?: number;
|
||||
code?: string | number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.35.0-nightly.20260313.bb060d7a9",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"description": "Gemini CLI Core",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -120,25 +120,4 @@ describe('agent-scheduler', () => {
|
||||
expect(schedulerConfig.toolRegistry).toBe(agentRegistry);
|
||||
expect(schedulerConfig.toolRegistry).not.toBe(mainRegistry);
|
||||
});
|
||||
|
||||
it('should create an AgentLoopContext that has a defined .config property', async () => {
|
||||
const mockConfig = {
|
||||
messageBus: mockMessageBus,
|
||||
toolRegistry: mockToolRegistry,
|
||||
promptId: 'test-prompt',
|
||||
} as unknown as Mocked<Config>;
|
||||
|
||||
const options = {
|
||||
schedulerId: 'subagent-1',
|
||||
toolRegistry: mockToolRegistry as unknown as ToolRegistry,
|
||||
signal: new AbortController().signal,
|
||||
};
|
||||
|
||||
await scheduleAgentTools(mockConfig as unknown as Config, [], options);
|
||||
|
||||
const schedulerContext = vi.mocked(Scheduler).mock.calls[0][0].context;
|
||||
expect(schedulerContext.config).toBeDefined();
|
||||
expect(schedulerContext.config.promptId).toBe('test-prompt');
|
||||
expect(schedulerContext.toolRegistry).toBe(mockToolRegistry);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,17 +67,8 @@ export async function scheduleAgentTools(
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const schedulerContext = {
|
||||
config: agentConfig,
|
||||
promptId: config.promptId,
|
||||
toolRegistry,
|
||||
messageBus: toolRegistry.messageBus,
|
||||
geminiClient: config.geminiClient,
|
||||
sandboxManager: config.sandboxManager,
|
||||
};
|
||||
|
||||
const scheduler = new Scheduler({
|
||||
context: schedulerContext,
|
||||
context: agentConfig,
|
||||
messageBus: toolRegistry.messageBus,
|
||||
getPreferredEditor: getPreferredEditor ?? (() => undefined),
|
||||
schedulerId,
|
||||
|
||||
@@ -53,22 +53,9 @@ When you need to identify elements by visual attributes not in the AX tree (e.g.
|
||||
* Extracted from prototype (computer_use_subagent_cdt branch).
|
||||
*
|
||||
* @param visionEnabled Whether visual tools (analyze_screenshot, click_at) are available.
|
||||
* @param allowedDomains Optional list of allowed domains to restrict navigation.
|
||||
*/
|
||||
export function buildBrowserSystemPrompt(
|
||||
visionEnabled: boolean,
|
||||
allowedDomains?: string[],
|
||||
): string {
|
||||
const allowedDomainsInstruction =
|
||||
allowedDomains && allowedDomains.length > 0
|
||||
? `\n\nSECURITY DOMAIN RESTRICTION - CRITICAL:\nYou are strictly limited to the following allowed domains (and their subdomains if specified with '*.'):\n${allowedDomains
|
||||
.map((d) => `- ${d}`)
|
||||
.join(
|
||||
'\n',
|
||||
)}\nDo NOT attempt to navigate to any other domains using new_page or navigate_page, as it will be rejected. This is a hard security constraint.`
|
||||
: '';
|
||||
|
||||
return `You are an expert browser automation agent (Orchestrator). Your goal is to completely fulfill the user's request.${allowedDomainsInstruction}
|
||||
export function buildBrowserSystemPrompt(visionEnabled: boolean): string {
|
||||
return `You are an expert browser automation agent (Orchestrator). Your goal is to completely fulfill the user's request.
|
||||
|
||||
IMPORTANT: You will receive an accessibility tree snapshot showing elements with uid values (e.g., uid=87_4 button "Login").
|
||||
Use these uid values directly with your tools:
|
||||
@@ -122,7 +109,7 @@ export const BrowserAgentDefinition = (
|
||||
): LocalAgentDefinition<typeof BrowserTaskResultSchema> => {
|
||||
// Use Preview Flash model if the main model is any of the preview models.
|
||||
// If the main model is not a preview model, use the default flash model.
|
||||
const model = isPreviewModel(config.getModel())
|
||||
const model = isPreviewModel(config.getModel(), config)
|
||||
? PREVIEW_GEMINI_FLASH_MODEL
|
||||
: DEFAULT_GEMINI_FLASH_MODEL;
|
||||
|
||||
@@ -179,10 +166,7 @@ export const BrowserAgentDefinition = (
|
||||
</task>
|
||||
|
||||
First, use new_page to open the relevant URL. Then call take_snapshot to see the page and proceed with your task.`,
|
||||
systemPrompt: buildBrowserSystemPrompt(
|
||||
visionEnabled,
|
||||
config.getBrowserAgentConfig().customConfig.allowedDomains,
|
||||
),
|
||||
systemPrompt: buildBrowserSystemPrompt(visionEnabled),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -239,25 +239,6 @@ describe('browserAgentFactory', () => {
|
||||
expect(toolNames).toContain('analyze_screenshot');
|
||||
});
|
||||
|
||||
it('should include domain restrictions in system prompt when configured', async () => {
|
||||
const configWithDomains = makeFakeConfig({
|
||||
agents: {
|
||||
browser: {
|
||||
allowedDomains: ['restricted.com'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { definition } = await createBrowserAgentDefinition(
|
||||
configWithDomains,
|
||||
mockMessageBus,
|
||||
);
|
||||
|
||||
const systemPrompt = definition.promptConfig?.systemPrompt ?? '';
|
||||
expect(systemPrompt).toContain('SECURITY DOMAIN RESTRICTION - CRITICAL:');
|
||||
expect(systemPrompt).toContain('- restricted.com');
|
||||
});
|
||||
|
||||
it('should include all MCP navigation tools (new_page, navigate_page) in definition', async () => {
|
||||
mockBrowserManager.getDiscoveredTools.mockResolvedValue([
|
||||
{ name: 'take_snapshot', description: 'Take snapshot' },
|
||||
@@ -342,22 +323,4 @@ describe('buildBrowserSystemPrompt', () => {
|
||||
expect(prompt).toContain('complete_task');
|
||||
}
|
||||
});
|
||||
|
||||
it('should include allowed domains restriction when provided', () => {
|
||||
const prompt = buildBrowserSystemPrompt(false, [
|
||||
'github.com',
|
||||
'*.google.com',
|
||||
]);
|
||||
expect(prompt).toContain('SECURITY DOMAIN RESTRICTION - CRITICAL:');
|
||||
expect(prompt).toContain('- github.com');
|
||||
expect(prompt).toContain('- *.google.com');
|
||||
});
|
||||
|
||||
it('should exclude allowed domains restriction when not provided or empty', () => {
|
||||
let prompt = buildBrowserSystemPrompt(false);
|
||||
expect(prompt).not.toContain('SECURITY DOMAIN RESTRICTION - CRITICAL:');
|
||||
|
||||
prompt = buildBrowserSystemPrompt(false, []);
|
||||
expect(prompt).not.toContain('SECURITY DOMAIN RESTRICTION - CRITICAL:');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -143,75 +143,6 @@ describe('BrowserManager', () => {
|
||||
isError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should block navigate_page to disallowed domain', async () => {
|
||||
const restrictedConfig = makeFakeConfig({
|
||||
agents: {
|
||||
browser: {
|
||||
allowedDomains: ['google.com'],
|
||||
},
|
||||
},
|
||||
});
|
||||
const manager = new BrowserManager(restrictedConfig);
|
||||
const result = await manager.callTool('navigate_page', {
|
||||
url: 'https://evil.com',
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect((result.content || [])[0]?.text).toContain('not permitted');
|
||||
expect(Client).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow navigate_page to allowed domain', async () => {
|
||||
const restrictedConfig = makeFakeConfig({
|
||||
agents: {
|
||||
browser: {
|
||||
allowedDomains: ['google.com'],
|
||||
},
|
||||
},
|
||||
});
|
||||
const manager = new BrowserManager(restrictedConfig);
|
||||
const result = await manager.callTool('navigate_page', {
|
||||
url: 'https://google.com/search',
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(false);
|
||||
expect((result.content || [])[0]?.text).toBe('Tool result');
|
||||
});
|
||||
|
||||
it('should allow navigate_page to subdomain when wildcard is used', async () => {
|
||||
const restrictedConfig = makeFakeConfig({
|
||||
agents: {
|
||||
browser: {
|
||||
allowedDomains: ['*.google.com'],
|
||||
},
|
||||
},
|
||||
});
|
||||
const manager = new BrowserManager(restrictedConfig);
|
||||
const result = await manager.callTool('navigate_page', {
|
||||
url: 'https://mail.google.com',
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(false);
|
||||
expect((result.content || [])[0]?.text).toBe('Tool result');
|
||||
});
|
||||
|
||||
it('should block new_page to disallowed domain', async () => {
|
||||
const restrictedConfig = makeFakeConfig({
|
||||
agents: {
|
||||
browser: {
|
||||
allowedDomains: ['google.com'],
|
||||
},
|
||||
},
|
||||
});
|
||||
const manager = new BrowserManager(restrictedConfig);
|
||||
const result = await manager.callTool('new_page', {
|
||||
url: 'https://evil.com',
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect((result.content || [])[0]?.text).toContain('not permitted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP connection', () => {
|
||||
@@ -241,40 +172,6 @@ describe('BrowserManager', () => {
|
||||
expect(args[userDataDirIndex + 1]).toMatch(/cli-browser-profile$/);
|
||||
});
|
||||
|
||||
it('should pass --host-rules when allowedDomains is configured', async () => {
|
||||
const restrictedConfig = makeFakeConfig({
|
||||
agents: {
|
||||
browser: {
|
||||
allowedDomains: ['google.com', '*.openai.com'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const manager = new BrowserManager(restrictedConfig);
|
||||
await manager.ensureConnection();
|
||||
|
||||
const args = vi.mocked(StdioClientTransport).mock.calls[0]?.[0]
|
||||
?.args as string[];
|
||||
expect(args).toContain(
|
||||
'--chromeArg="--host-rules=MAP * 127.0.0.1, EXCLUDE google.com, EXCLUDE *.openai.com, EXCLUDE 127.0.0.1"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when invalid domain is configured in allowedDomains', async () => {
|
||||
const invalidConfig = makeFakeConfig({
|
||||
agents: {
|
||||
browser: {
|
||||
allowedDomains: ['invalid domain!'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const manager = new BrowserManager(invalidConfig);
|
||||
await expect(manager.ensureConnection()).rejects.toThrow(
|
||||
'Invalid domain in allowedDomains: invalid domain!',
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass headless flag when configured', async () => {
|
||||
const headlessConfig = makeFakeConfig({
|
||||
agents: {
|
||||
|
||||
@@ -147,19 +147,6 @@ export class BrowserManager {
|
||||
throw signal.reason ?? new Error('Operation cancelled');
|
||||
}
|
||||
|
||||
const errorMessage = this.checkNavigationRestrictions(toolName, args);
|
||||
if (errorMessage) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: errorMessage,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const client = await this.getRawMcpClient();
|
||||
const callPromise = client.callTool(
|
||||
{ name: toolName, arguments: args },
|
||||
@@ -355,23 +342,6 @@ export class BrowserManager {
|
||||
mcpArgs.push('--userDataDir', defaultProfilePath);
|
||||
}
|
||||
|
||||
if (
|
||||
browserConfig.customConfig.allowedDomains &&
|
||||
browserConfig.customConfig.allowedDomains.length > 0
|
||||
) {
|
||||
const exclusionRules = browserConfig.customConfig.allowedDomains
|
||||
.map((domain) => {
|
||||
if (!/^(\*\.)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/.test(domain)) {
|
||||
throw new Error(`Invalid domain in allowedDomains: ${domain}`);
|
||||
}
|
||||
return `EXCLUDE ${domain}`;
|
||||
})
|
||||
.join(', ');
|
||||
mcpArgs.push(
|
||||
`--chromeArg="--host-rules=MAP * 127.0.0.1, ${exclusionRules}, EXCLUDE 127.0.0.1"`,
|
||||
);
|
||||
}
|
||||
|
||||
debugLogger.log(
|
||||
`Launching chrome-devtools-mcp (${sessionMode} mode) with args: ${mcpArgs.join(' ')}`,
|
||||
);
|
||||
@@ -532,63 +502,6 @@ export class BrowserManager {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check navigation restrictions based on tools and the args sent
|
||||
* along with them.
|
||||
*
|
||||
* @returns error message if failed, undefined if passed.
|
||||
*/
|
||||
private checkNavigationRestrictions(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
): string | undefined {
|
||||
const pageNavigationTools = ['navigate_page', 'new_page'];
|
||||
|
||||
if (!pageNavigationTools.includes(toolName)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const allowedDomains =
|
||||
this.config.getBrowserAgentConfig().customConfig.allowedDomains;
|
||||
if (!allowedDomains || allowedDomains.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const url = args['url'];
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof url !== 'string') {
|
||||
return `Invalid URL: URL must be a string.`;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
const urlHostname = parsedUrl.hostname.replace(/\.$/, '');
|
||||
|
||||
for (const domainPattern of allowedDomains) {
|
||||
if (domainPattern.startsWith('*.')) {
|
||||
const baseDomain = domainPattern.substring(2);
|
||||
if (
|
||||
urlHostname === baseDomain ||
|
||||
urlHostname.endsWith(`.${baseDomain}`)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
if (urlHostname === domainPattern) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return `Invalid URL: Malformed URL string.`;
|
||||
}
|
||||
|
||||
// If none matched, then deny
|
||||
return `Tool '${toolName}' is not permitted for the requested URL/domain based on your current browser settings.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a fallback notification handler on the MCP client to
|
||||
* automatically re-inject the input blocker after any server-side
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type ToolInvocation,
|
||||
type ToolResult,
|
||||
} from '../tools/tools.js';
|
||||
|
||||
import type { Config } from '../config/config.js';
|
||||
import { type AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
import type { AgentDefinition, AgentInputs } from './types.js';
|
||||
import { LocalSubagentInvocation } from './local-invocation.js';
|
||||
@@ -54,6 +54,10 @@ export class SubagentToolWrapper extends BaseDeclarativeTool<
|
||||
);
|
||||
}
|
||||
|
||||
private get config(): Config {
|
||||
return this.context.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an invocation instance for executing the subagent.
|
||||
*
|
||||
@@ -85,7 +89,7 @@ export class SubagentToolWrapper extends BaseDeclarativeTool<
|
||||
// Special handling for browser agent - needs async MCP setup
|
||||
if (definition.name === BROWSER_AGENT_NAME) {
|
||||
return new BrowserAgentInvocation(
|
||||
this.context,
|
||||
this.config,
|
||||
params,
|
||||
effectiveMessageBus,
|
||||
_toolName,
|
||||
|
||||
@@ -54,19 +54,21 @@ export function resolvePolicyChain(
|
||||
useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
);
|
||||
const isAutoPreferred = preferredModel ? isAutoModel(preferredModel) : false;
|
||||
const isAutoConfigured = isAutoModel(configuredModel);
|
||||
const isAutoPreferred = preferredModel
|
||||
? isAutoModel(preferredModel, config)
|
||||
: false;
|
||||
const isAutoConfigured = isAutoModel(configuredModel, config);
|
||||
|
||||
if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) {
|
||||
chain = getFlashLitePolicyChain();
|
||||
} else if (
|
||||
isGemini3Model(resolvedModel) ||
|
||||
isGemini3Model(resolvedModel, config) ||
|
||||
isAutoPreferred ||
|
||||
isAutoConfigured
|
||||
) {
|
||||
if (hasAccessToPreview) {
|
||||
const previewEnabled =
|
||||
isGemini3Model(resolvedModel) ||
|
||||
isGemini3Model(resolvedModel, config) ||
|
||||
preferredModel === PREVIEW_GEMINI_MODEL_AUTO ||
|
||||
configuredModel === PREVIEW_GEMINI_MODEL_AUTO;
|
||||
chain = getModelPolicyChain({
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import type { GeminiClient } from '../core/client.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import type { ToolRegistry } from '../tools/tool-registry.js';
|
||||
import type { SandboxManager } from '../services/sandboxManager.js';
|
||||
import type { Config } from './config.js';
|
||||
|
||||
/**
|
||||
@@ -29,7 +28,4 @@ export interface AgentLoopContext {
|
||||
|
||||
/** The client used to communicate with the LLM in this context. */
|
||||
readonly geminiClient: GeminiClient;
|
||||
|
||||
/** The service used to prepare commands for sandboxed execution. */
|
||||
readonly sandboxManager: SandboxManager;
|
||||
}
|
||||
|
||||
@@ -41,10 +41,6 @@ import { LocalLiteRtLmClient } from '../core/localLiteRtLmClient.js';
|
||||
import type { HookDefinition, HookEventName } from '../hooks/types.js';
|
||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||
import { GitService } from '../services/gitService.js';
|
||||
import {
|
||||
createSandboxManager,
|
||||
type SandboxManager,
|
||||
} from '../services/sandboxManager.js';
|
||||
import {
|
||||
initializeTelemetry,
|
||||
DEFAULT_TELEMETRY_TARGET,
|
||||
@@ -320,8 +316,6 @@ export interface BrowserAgentCustomConfig {
|
||||
profilePath?: string;
|
||||
/** Model override for the visual agent. */
|
||||
visualModel?: string;
|
||||
/** List of allowed domains for the browser agent (e.g., ["github.com", "*.google.com"]). */
|
||||
allowedDomains?: string[];
|
||||
/** Disable user input on the browser window during automation. Default: true in non-headless mode */
|
||||
disableUserInput?: boolean;
|
||||
}
|
||||
@@ -514,7 +508,6 @@ export interface ConfigParameters {
|
||||
clientVersion?: string;
|
||||
embeddingModel?: string;
|
||||
sandbox?: SandboxConfig;
|
||||
toolSandboxing?: boolean;
|
||||
targetDir: string;
|
||||
debugMode: boolean;
|
||||
question?: string;
|
||||
@@ -606,9 +599,9 @@ export interface ConfigParameters {
|
||||
recordResponses?: string;
|
||||
ptyInfo?: string;
|
||||
disableYoloMode?: boolean;
|
||||
disableAlwaysAllow?: boolean;
|
||||
rawOutput?: boolean;
|
||||
acceptRawOutputRisk?: boolean;
|
||||
dynamicModelConfiguration?: boolean;
|
||||
modelConfigServiceConfig?: ModelConfigServiceConfig;
|
||||
enableHooks?: boolean;
|
||||
enableHooksUI?: boolean;
|
||||
@@ -622,7 +615,6 @@ export interface ConfigParameters {
|
||||
disabledSkills?: string[];
|
||||
adminSkillsEnabled?: boolean;
|
||||
experimentalJitContext?: boolean;
|
||||
topicUpdateNarration?: boolean;
|
||||
toolOutputMasking?: Partial<ToolOutputMaskingConfig>;
|
||||
disableLLMCorrection?: boolean;
|
||||
plan?: boolean;
|
||||
@@ -693,7 +685,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly telemetrySettings: TelemetrySettings;
|
||||
private readonly usageStatisticsEnabled: boolean;
|
||||
private _geminiClient!: GeminiClient;
|
||||
private readonly _sandboxManager: SandboxManager;
|
||||
private baseLlmClient!: BaseLlmClient;
|
||||
private localLiteRtLmClient?: LocalLiteRtLmClient;
|
||||
private modelRouterService: ModelRouterService;
|
||||
@@ -807,9 +798,9 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
readonly fakeResponses?: string;
|
||||
readonly recordResponses?: string;
|
||||
private readonly disableYoloMode: boolean;
|
||||
private readonly disableAlwaysAllow: boolean;
|
||||
private readonly rawOutput: boolean;
|
||||
private readonly acceptRawOutputRisk: boolean;
|
||||
private readonly dynamicModelConfiguration: boolean;
|
||||
private pendingIncludeDirectories: string[];
|
||||
private readonly enableHooks: boolean;
|
||||
private readonly enableHooksUI: boolean;
|
||||
@@ -843,7 +834,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly adminSkillsEnabled: boolean;
|
||||
|
||||
private readonly experimentalJitContext: boolean;
|
||||
private readonly topicUpdateNarration: boolean;
|
||||
private readonly disableLLMCorrection: boolean;
|
||||
private readonly planEnabled: boolean;
|
||||
private readonly trackerEnabled: boolean;
|
||||
@@ -865,19 +855,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.embeddingModel =
|
||||
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
|
||||
this.fileSystemService = new StandardFileSystemService();
|
||||
this.sandbox = params.sandbox
|
||||
? {
|
||||
enabled: params.sandbox.enabled ?? false,
|
||||
allowedPaths: params.sandbox.allowedPaths ?? [],
|
||||
networkAccess: params.sandbox.networkAccess ?? false,
|
||||
command: params.sandbox.command,
|
||||
image: params.sandbox.image,
|
||||
}
|
||||
: {
|
||||
enabled: false,
|
||||
allowedPaths: [],
|
||||
networkAccess: false,
|
||||
};
|
||||
this.sandbox = params.sandbox;
|
||||
this.targetDir = path.resolve(params.targetDir);
|
||||
this.folderTrust = params.folderTrust ?? false;
|
||||
this.workspaceContext = new WorkspaceContext(this.targetDir, []);
|
||||
@@ -958,7 +936,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.adminSkillsEnabled = params.adminSkillsEnabled ?? true;
|
||||
this.modelAvailabilityService = new ModelAvailabilityService();
|
||||
this.experimentalJitContext = params.experimentalJitContext ?? false;
|
||||
this.topicUpdateNarration = params.topicUpdateNarration ?? false;
|
||||
this.modelSteering = params.modelSteering ?? false;
|
||||
this.userHintService = new UserHintService(() =>
|
||||
this.isModelSteeringEnabled(),
|
||||
@@ -1008,12 +985,11 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
showColor: params.shellExecutionConfig?.showColor ?? false,
|
||||
pager: params.shellExecutionConfig?.pager ?? 'cat',
|
||||
sanitizationConfig: this.sanitizationConfig,
|
||||
sandboxManager: this.sandboxManager,
|
||||
};
|
||||
this.truncateToolOutputThreshold =
|
||||
params.truncateToolOutputThreshold ??
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD;
|
||||
this.useWriteTodos = isPreviewModel(this.model)
|
||||
this.useWriteTodos = isPreviewModel(this.model, this)
|
||||
? false
|
||||
: (params.useWriteTodos ?? true);
|
||||
this.workspacePoliciesDir = params.workspacePoliciesDir;
|
||||
@@ -1050,13 +1026,11 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.policyUpdateConfirmationRequest =
|
||||
params.policyUpdateConfirmationRequest;
|
||||
|
||||
this.disableAlwaysAllow = params.disableAlwaysAllow ?? false;
|
||||
this.policyEngine = new PolicyEngine(
|
||||
{
|
||||
...params.policyEngineConfig,
|
||||
approvalMode:
|
||||
params.approvalMode ?? params.policyEngineConfig?.approvalMode,
|
||||
disableAlwaysAllow: this.disableAlwaysAllow,
|
||||
},
|
||||
checkerRunner,
|
||||
);
|
||||
@@ -1090,6 +1064,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.disableYoloMode = params.disableYoloMode ?? false;
|
||||
this.rawOutput = params.rawOutput ?? false;
|
||||
this.acceptRawOutputRisk = params.acceptRawOutputRisk ?? false;
|
||||
this.dynamicModelConfiguration = params.dynamicModelConfiguration ?? false;
|
||||
|
||||
if (params.hooks) {
|
||||
this.hooks = params.hooks;
|
||||
@@ -1128,8 +1103,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
}
|
||||
}
|
||||
this._geminiClient = new GeminiClient(this);
|
||||
this._sandboxManager = createSandboxManager(params.toolSandboxing ?? false);
|
||||
this.shellExecutionConfig.sandboxManager = this._sandboxManager;
|
||||
this.modelRouterService = new ModelRouterService(this);
|
||||
|
||||
// HACK: The settings loading logic doesn't currently merge the default
|
||||
@@ -1141,18 +1114,23 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
// remove this hack.
|
||||
let modelConfigServiceConfig = params.modelConfigServiceConfig;
|
||||
if (modelConfigServiceConfig) {
|
||||
if (!modelConfigServiceConfig.aliases) {
|
||||
modelConfigServiceConfig = {
|
||||
...modelConfigServiceConfig,
|
||||
aliases: DEFAULT_MODEL_CONFIGS.aliases,
|
||||
};
|
||||
}
|
||||
if (!modelConfigServiceConfig.overrides) {
|
||||
modelConfigServiceConfig = {
|
||||
...modelConfigServiceConfig,
|
||||
overrides: DEFAULT_MODEL_CONFIGS.overrides,
|
||||
};
|
||||
}
|
||||
// Ensure user-defined model definitions augment, not replace, the defaults.
|
||||
const mergedModelDefinitions = {
|
||||
...DEFAULT_MODEL_CONFIGS.modelDefinitions,
|
||||
...modelConfigServiceConfig.modelDefinitions,
|
||||
};
|
||||
|
||||
modelConfigServiceConfig = {
|
||||
// Preserve other user settings like customAliases
|
||||
...modelConfigServiceConfig,
|
||||
// Apply defaults for aliases and overrides if they are not provided
|
||||
aliases:
|
||||
modelConfigServiceConfig.aliases ?? DEFAULT_MODEL_CONFIGS.aliases,
|
||||
overrides:
|
||||
modelConfigServiceConfig.overrides ?? DEFAULT_MODEL_CONFIGS.overrides,
|
||||
// Use the merged model definitions
|
||||
modelDefinitions: mergedModelDefinitions,
|
||||
};
|
||||
}
|
||||
|
||||
this.modelConfigService = new ModelConfigService(
|
||||
@@ -1355,7 +1333,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
|
||||
// Only reset when we have explicit "no access" (hasAccessToPreviewModel === false).
|
||||
// When null (quota not fetched) or true, we preserve the saved model.
|
||||
if (isPreviewModel(this.model) && this.hasAccessToPreviewModel === false) {
|
||||
if (
|
||||
isPreviewModel(this.model, this) &&
|
||||
this.hasAccessToPreviewModel === false
|
||||
) {
|
||||
this.setModel(DEFAULT_GEMINI_MODEL_AUTO);
|
||||
}
|
||||
|
||||
@@ -1451,10 +1432,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this._geminiClient;
|
||||
}
|
||||
|
||||
get sandboxManager(): SandboxManager {
|
||||
return this._sandboxManager;
|
||||
}
|
||||
|
||||
getSessionId(): string {
|
||||
return this.promptId;
|
||||
}
|
||||
@@ -1627,7 +1604,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
|
||||
const isPreview =
|
||||
model === PREVIEW_GEMINI_MODEL_AUTO ||
|
||||
isPreviewModel(this.getActiveModel());
|
||||
isPreviewModel(this.getActiveModel(), this);
|
||||
const proModel = isPreview ? PREVIEW_GEMINI_MODEL : DEFAULT_GEMINI_MODEL;
|
||||
const flashModel = isPreview
|
||||
? PREVIEW_GEMINI_FLASH_MODEL
|
||||
@@ -1825,8 +1802,9 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
}
|
||||
|
||||
const hasAccess =
|
||||
quota.buckets?.some((b) => b.modelId && isPreviewModel(b.modelId)) ??
|
||||
false;
|
||||
quota.buckets?.some(
|
||||
(b) => b.modelId && isPreviewModel(b.modelId, this),
|
||||
) ?? false;
|
||||
this.setHasAccessToPreviewModel(hasAccess);
|
||||
return quota;
|
||||
} catch (e) {
|
||||
@@ -2048,10 +2026,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.experimentalJitContext;
|
||||
}
|
||||
|
||||
isTopicUpdateNarrationEnabled(): boolean {
|
||||
return this.topicUpdateNarration;
|
||||
}
|
||||
|
||||
isModelSteeringEnabled(): boolean {
|
||||
return this.modelSteering;
|
||||
}
|
||||
@@ -2214,10 +2188,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.disableYoloMode || !this.isTrustedFolder();
|
||||
}
|
||||
|
||||
getDisableAlwaysAllow(): boolean {
|
||||
return this.disableAlwaysAllow;
|
||||
}
|
||||
|
||||
getRawOutput(): boolean {
|
||||
return this.rawOutput;
|
||||
}
|
||||
@@ -2226,6 +2196,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.acceptRawOutputRisk;
|
||||
}
|
||||
|
||||
getExperimentalDynamicModelConfiguration(): boolean {
|
||||
return this.dynamicModelConfiguration;
|
||||
}
|
||||
|
||||
getPendingIncludeDirectories(): string[] {
|
||||
return this.pendingIncludeDirectories;
|
||||
}
|
||||
@@ -2850,8 +2824,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
sanitizationConfig:
|
||||
config.sanitizationConfig ??
|
||||
this.shellExecutionConfig.sanitizationConfig,
|
||||
sandboxManager:
|
||||
config.sandboxManager ?? this.shellExecutionConfig.sandboxManager,
|
||||
};
|
||||
}
|
||||
getScreenReader(): boolean {
|
||||
@@ -2946,7 +2918,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
headless: customConfig.headless ?? false,
|
||||
profilePath: customConfig.profilePath,
|
||||
visualModel: customConfig.visualModel,
|
||||
allowedDomains: customConfig.allowedDomains,
|
||||
disableUserInput: customConfig.disableUserInput,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -249,4 +249,94 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
modelDefinitions: {
|
||||
// Concrete Models
|
||||
'gemini-3.1-pro-preview': {
|
||||
tier: 'pro',
|
||||
family: 'gemini-3',
|
||||
isPreview: true,
|
||||
dialogLocation: 'manual',
|
||||
features: { thinking: true, multimodalToolUse: true },
|
||||
},
|
||||
'gemini-3.1-pro-preview-customtools': {
|
||||
tier: 'pro',
|
||||
family: 'gemini-3',
|
||||
isPreview: true,
|
||||
features: { thinking: true, multimodalToolUse: true },
|
||||
},
|
||||
'gemini-3-pro-preview': {
|
||||
tier: 'pro',
|
||||
family: 'gemini-3',
|
||||
isPreview: true,
|
||||
dialogLocation: 'manual',
|
||||
features: { thinking: true, multimodalToolUse: true },
|
||||
},
|
||||
'gemini-3-flash-preview': {
|
||||
tier: 'flash',
|
||||
family: 'gemini-3',
|
||||
isPreview: true,
|
||||
dialogLocation: 'manual',
|
||||
features: { thinking: false, multimodalToolUse: true },
|
||||
},
|
||||
'gemini-2.5-pro': {
|
||||
tier: 'pro',
|
||||
family: 'gemini-2.5',
|
||||
isPreview: false,
|
||||
dialogLocation: 'manual',
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
'gemini-2.5-flash': {
|
||||
tier: 'flash',
|
||||
family: 'gemini-2.5',
|
||||
isPreview: false,
|
||||
dialogLocation: 'manual',
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
'gemini-2.5-flash-lite': {
|
||||
tier: 'flash-lite',
|
||||
family: 'gemini-2.5',
|
||||
isPreview: false,
|
||||
dialogLocation: 'manual',
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
// Aliases
|
||||
auto: {
|
||||
tier: 'auto',
|
||||
isPreview: true,
|
||||
features: { thinking: true, multimodalToolUse: false },
|
||||
},
|
||||
pro: {
|
||||
tier: 'pro',
|
||||
isPreview: false,
|
||||
features: { thinking: true, multimodalToolUse: false },
|
||||
},
|
||||
flash: {
|
||||
tier: 'flash',
|
||||
isPreview: false,
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
'flash-lite': {
|
||||
tier: 'flash-lite',
|
||||
isPreview: false,
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
'auto-gemini-3': {
|
||||
displayName: 'Auto (Gemini 3)',
|
||||
tier: 'auto',
|
||||
isPreview: true,
|
||||
dialogLocation: 'main',
|
||||
dialogDescription:
|
||||
'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash',
|
||||
features: { thinking: true, multimodalToolUse: false },
|
||||
},
|
||||
'auto-gemini-2.5': {
|
||||
displayName: 'Auto (Gemini 2.5)',
|
||||
tier: 'auto',
|
||||
isPreview: false,
|
||||
dialogLocation: 'main',
|
||||
dialogDescription:
|
||||
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
GEMINI_MODEL_ALIAS_PRO,
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
GEMINI_MODEL_ALIAS_FLASH_LITE,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
@@ -30,7 +31,126 @@ import {
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
isPreviewModel,
|
||||
isProModel,
|
||||
isValidModelOrAlias,
|
||||
getValidModelsAndAliases,
|
||||
VALID_GEMINI_MODELS,
|
||||
VALID_ALIASES,
|
||||
} from './models.js';
|
||||
import type { Config } from './config.js';
|
||||
import { ModelConfigService } from '../services/modelConfigService.js';
|
||||
import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js';
|
||||
|
||||
const modelConfigService = new ModelConfigService(DEFAULT_MODEL_CONFIGS);
|
||||
|
||||
const dynamicConfig = {
|
||||
getExperimentalDynamicModelConfiguration: () => true,
|
||||
modelConfigService,
|
||||
} as unknown as Config;
|
||||
|
||||
const legacyConfig = {
|
||||
getExperimentalDynamicModelConfiguration: () => false,
|
||||
modelConfigService,
|
||||
} as unknown as Config;
|
||||
|
||||
describe('Dynamic Configuration Parity', () => {
|
||||
const modelsToTest = [
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
GEMINI_MODEL_ALIAS_PRO,
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
GEMINI_MODEL_ALIAS_FLASH_LITE,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
'custom-model',
|
||||
];
|
||||
|
||||
it('getDisplayString should match legacy behavior', () => {
|
||||
for (const model of modelsToTest) {
|
||||
const legacy = getDisplayString(model, legacyConfig);
|
||||
const dynamic = getDisplayString(model, dynamicConfig);
|
||||
expect(dynamic).toBe(legacy);
|
||||
}
|
||||
});
|
||||
|
||||
it('isPreviewModel should match legacy behavior', () => {
|
||||
const allModels = [
|
||||
...modelsToTest,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
];
|
||||
for (const model of allModels) {
|
||||
const legacy = isPreviewModel(model, legacyConfig);
|
||||
const dynamic = isPreviewModel(model, dynamicConfig);
|
||||
expect(dynamic).toBe(legacy);
|
||||
}
|
||||
});
|
||||
|
||||
it('isProModel should match legacy behavior', () => {
|
||||
for (const model of modelsToTest) {
|
||||
const legacy = isProModel(model, legacyConfig);
|
||||
const dynamic = isProModel(model, dynamicConfig);
|
||||
expect(dynamic).toBe(legacy);
|
||||
}
|
||||
});
|
||||
|
||||
it('isGemini3Model should match legacy behavior', () => {
|
||||
for (const model of modelsToTest) {
|
||||
const legacy = isGemini3Model(model, legacyConfig);
|
||||
const dynamic = isGemini3Model(model, dynamicConfig);
|
||||
expect(dynamic).toBe(legacy);
|
||||
}
|
||||
});
|
||||
|
||||
it('isGemini2Model should match legacy behavior', () => {
|
||||
for (const model of modelsToTest) {
|
||||
const legacy = isGemini2Model(model, legacyConfig);
|
||||
const dynamic = isGemini2Model(model, dynamicConfig);
|
||||
expect(dynamic).toBe(legacy);
|
||||
}
|
||||
});
|
||||
|
||||
it('isCustomModel should match legacy behavior', () => {
|
||||
for (const model of modelsToTest) {
|
||||
const legacy = isCustomModel(model, legacyConfig);
|
||||
const dynamic = isCustomModel(model, dynamicConfig);
|
||||
expect(dynamic).toBe(legacy);
|
||||
}
|
||||
});
|
||||
|
||||
it('supportsModernFeatures should match legacy behavior', () => {
|
||||
for (const model of modelsToTest) {
|
||||
const legacy = supportsModernFeatures(model, legacyConfig);
|
||||
const dynamic = supportsModernFeatures(model, dynamicConfig);
|
||||
expect(dynamic).toBe(legacy);
|
||||
}
|
||||
});
|
||||
|
||||
it('isActiveModel should match legacy behavior', () => {
|
||||
for (const model of modelsToTest) {
|
||||
const legacy = isActiveModel(model, false, false, legacyConfig);
|
||||
const dynamic = isActiveModel(model, false, false, dynamicConfig);
|
||||
expect(dynamic).toBe(legacy);
|
||||
}
|
||||
});
|
||||
|
||||
it('isValidModelOrAlias should match legacy behavior', () => {
|
||||
for (const model of modelsToTest) {
|
||||
const legacy = isValidModelOrAlias(model, legacyConfig);
|
||||
const dynamic = isValidModelOrAlias(model, dynamicConfig);
|
||||
expect(dynamic).toBe(legacy);
|
||||
}
|
||||
});
|
||||
|
||||
it('supportsMultimodalFunctionResponse should match legacy behavior', () => {
|
||||
for (const model of modelsToTest) {
|
||||
const legacy = supportsMultimodalFunctionResponse(model, legacyConfig);
|
||||
const dynamic = supportsMultimodalFunctionResponse(model, dynamicConfig);
|
||||
expect(dynamic).toBe(legacy);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPreviewModel', () => {
|
||||
it('should return true for preview models', () => {
|
||||
@@ -389,3 +509,62 @@ describe('isActiveModel', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidModelOrAlias', () => {
|
||||
it('should return true for valid model names', () => {
|
||||
expect(isValidModelOrAlias(DEFAULT_GEMINI_MODEL)).toBe(true);
|
||||
expect(isValidModelOrAlias(PREVIEW_GEMINI_MODEL)).toBe(true);
|
||||
expect(isValidModelOrAlias(DEFAULT_GEMINI_FLASH_MODEL)).toBe(true);
|
||||
expect(isValidModelOrAlias(DEFAULT_GEMINI_FLASH_LITE_MODEL)).toBe(true);
|
||||
expect(isValidModelOrAlias(PREVIEW_GEMINI_FLASH_MODEL)).toBe(true);
|
||||
expect(isValidModelOrAlias(PREVIEW_GEMINI_3_1_MODEL)).toBe(true);
|
||||
expect(isValidModelOrAlias(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true for valid aliases', () => {
|
||||
expect(isValidModelOrAlias(GEMINI_MODEL_ALIAS_AUTO)).toBe(true);
|
||||
expect(isValidModelOrAlias(GEMINI_MODEL_ALIAS_PRO)).toBe(true);
|
||||
expect(isValidModelOrAlias(GEMINI_MODEL_ALIAS_FLASH)).toBe(true);
|
||||
expect(isValidModelOrAlias(GEMINI_MODEL_ALIAS_FLASH_LITE)).toBe(true);
|
||||
expect(isValidModelOrAlias(PREVIEW_GEMINI_MODEL_AUTO)).toBe(true);
|
||||
expect(isValidModelOrAlias(DEFAULT_GEMINI_MODEL_AUTO)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for custom (non-gemini) models', () => {
|
||||
expect(isValidModelOrAlias('gpt-4')).toBe(true);
|
||||
expect(isValidModelOrAlias('claude-3')).toBe(true);
|
||||
expect(isValidModelOrAlias('my-custom-model')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid gemini model names', () => {
|
||||
expect(isValidModelOrAlias('gemini-4-pro')).toBe(false);
|
||||
expect(isValidModelOrAlias('gemini-99-flash')).toBe(false);
|
||||
expect(isValidModelOrAlias('gemini-invalid')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValidModelsAndAliases', () => {
|
||||
it('should return a sorted array', () => {
|
||||
const result = getValidModelsAndAliases();
|
||||
const sorted = [...result].sort();
|
||||
expect(result).toEqual(sorted);
|
||||
});
|
||||
|
||||
it('should include all valid models and aliases', () => {
|
||||
const result = getValidModelsAndAliases();
|
||||
for (const model of VALID_GEMINI_MODELS) {
|
||||
expect(result).toContain(model);
|
||||
}
|
||||
for (const alias of VALID_ALIASES) {
|
||||
expect(result).toContain(alias);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not contain duplicates', () => {
|
||||
const result = getValidModelsAndAliases();
|
||||
const unique = [...new Set(result)];
|
||||
expect(result).toEqual(unique);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user