mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 08:10:57 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a30cc4a8a9 | |||
| 0e169ee968 | |||
| 6c23f0c7e1 | |||
| 83792d51c1 |
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"experimental": {
|
||||
"plan": true,
|
||||
"extensionReloading": true,
|
||||
"modelSteering": true,
|
||||
"memoryManager": false,
|
||||
"topicUpdateNarration": true
|
||||
"memoryManager": true
|
||||
},
|
||||
"general": {
|
||||
"devtools": true
|
||||
|
||||
@@ -1,194 +1,190 @@
|
||||
#!/bin/bash
|
||||
|
||||
notify() {
|
||||
local title="${1}"
|
||||
local message="${2}"
|
||||
local pr="${3}"
|
||||
local title="$1"
|
||||
local message="$2"
|
||||
local pr="$3"
|
||||
# Terminal escape sequence
|
||||
printf "\e]9;%s | PR #%s | %s\a" "${title}" "${pr}" "${message}"
|
||||
printf "\e]9;%s | PR #%s | %s\a" "$title" "$pr" "$message"
|
||||
# Native macOS notification
|
||||
os_type="$(uname || true)"
|
||||
if [[ "${os_type}" == "Darwin" ]]; then
|
||||
osascript -e "display notification \"${message}\" with title \"${title}\" subtitle \"PR #${pr}\""
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
osascript -e "display notification \"$message\" with title \"$title\" subtitle \"PR #$pr\""
|
||||
fi
|
||||
}
|
||||
|
||||
pr_number="${1}"
|
||||
if [[ -z "${pr_number}" ]]; then
|
||||
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 || true)"
|
||||
if [[ -z "${base_dir}" ]]; then
|
||||
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"
|
||||
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
|
||||
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"
|
||||
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 "🧹 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}"
|
||||
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}"
|
||||
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"
|
||||
echo "🌿 Worktree already exists." | tee -a "$log_dir/setup.log"
|
||||
fi
|
||||
echo 0 > "${log_dir}/setup.exit"
|
||||
echo 0 > "$log_dir/setup.exit"
|
||||
|
||||
cd "${target_dir}" || exit 1
|
||||
cd "$target_dir" || exit 1
|
||||
|
||||
echo "🚀 Launching background tasks. Logs saving to: ${log_dir}"
|
||||
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"; } &
|
||||
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"; } &
|
||||
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="$(command -v gemini || echo "${HOME}/.gcli/nightly/node_modules/.bin/gemini")"
|
||||
# shellcheck disable=SC2312
|
||||
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"; } &
|
||||
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"
|
||||
rm -f "$log_dir/npm-test.exit"
|
||||
{
|
||||
while [[ ! -f "${log_dir}/build-and-lint.exit" ]]; do sleep 1; done
|
||||
read -r build_exit < "${log_dir}/build-and-lint.exit" || build_exit=""
|
||||
if [[ "${build_exit}" == "0" ]]; then
|
||||
gh pr checks "${pr_number}" > "${log_dir}/ci-checks.log" 2>&1
|
||||
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"
|
||||
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 "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 || true)"
|
||||
run_id="$(gh run list --branch "${pr_branch}" --workflow ci.yml --json databaseId -q '.[0].databaseId' 2>/dev/null || true)"
|
||||
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 || true)"
|
||||
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"
|
||||
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)"
|
||||
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)"
|
||||
ws_dir=$(echo "$file" | cut -d'/' -f1)
|
||||
fi
|
||||
rel_file="${file#"${ws_dir}"/}"
|
||||
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
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
rm -f "$log_dir/test-execution.exit"
|
||||
{
|
||||
while [[ ! -f "${log_dir}/build-and-lint.exit" ]]; do sleep 1; done
|
||||
read -r build_exit < "${log_dir}/build-and-lint.exit" || build_exit=""
|
||||
if [[ "${build_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"
|
||||
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"
|
||||
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"
|
||||
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
|
||||
for t in "${tasks[@]}"; do task_done[$t]=0; done
|
||||
|
||||
all_done=0
|
||||
while [[ "${all_done}" -eq 0 ]]; do
|
||||
while [[ $all_done -eq 0 ]]; do
|
||||
clear
|
||||
echo "=================================================="
|
||||
echo "🚀 Async PR Review Status for PR #${pr_number}"
|
||||
echo "🚀 Async PR Review Status for PR #$pr_number"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
|
||||
all_done=1
|
||||
for i in "${!tasks[@]}"; do
|
||||
t="${tasks[${i}]}"
|
||||
t="${tasks[$i]}"
|
||||
|
||||
if [[ -f "${log_dir}/${t}.exit" ]]; then
|
||||
read -r task_exit < "${log_dir}/${t}.exit" || task_exit=""
|
||||
if [[ "${task_exit}" == "0" ]]; then
|
||||
echo " ✅ ${t}: SUCCESS"
|
||||
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 ${task_exit})"
|
||||
echo " ❌ $t: FAILED (exit code $exit_code)"
|
||||
fi
|
||||
task_done[${t}]=1
|
||||
task_done[$t]=1
|
||||
else
|
||||
echo " ⏳ ${t}: RUNNING"
|
||||
echo " ⏳ $t: RUNNING"
|
||||
all_done=0
|
||||
fi
|
||||
done
|
||||
@@ -199,47 +195,47 @@ while [[ "${all_done}" -eq 0 ]]; do
|
||||
echo "=================================================="
|
||||
|
||||
for i in "${!tasks[@]}"; do
|
||||
t="${tasks[${i}]}"
|
||||
log_file="${log_files[${i}]}"
|
||||
t="${tasks[$i]}"
|
||||
log_file="${log_files[$i]}"
|
||||
|
||||
if [[ "${task_done[${t}]}" -eq 0 ]]; then
|
||||
if [[ -f "${log_dir}/${log_file}" ]]; then
|
||||
if [[ ${task_done[$t]} -eq 0 ]]; then
|
||||
if [[ -f "$log_dir/$log_file" ]]; then
|
||||
echo ""
|
||||
echo "--- ${t} ---"
|
||||
tail -n 5 "${log_dir}/${log_file}"
|
||||
echo "--- $t ---"
|
||||
tail -n 5 "$log_dir/$log_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${all_done}" -eq 0 ]]; then
|
||||
if [[ $all_done -eq 0 ]]; then
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
|
||||
clear
|
||||
echo "=================================================="
|
||||
echo "🚀 Async PR Review Status for PR #${pr_number}"
|
||||
echo "🚀 Async PR Review Status for PR #$pr_number"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
for t in "${tasks[@]}"; do
|
||||
read -r task_exit < "${log_dir}/${t}.exit" || task_exit=""
|
||||
if [[ "${task_exit}" == "0" ]]; then
|
||||
echo " ✅ ${t}: SUCCESS"
|
||||
exit_code=$(cat "$log_dir/$t.exit")
|
||||
if [[ "$exit_code" == "0" ]]; then
|
||||
echo " ✅ $t: SUCCESS"
|
||||
else
|
||||
echo " ❌ ${t}: FAILED (exit code ${task_exit})"
|
||||
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"
|
||||
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}"
|
||||
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}"
|
||||
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,22 +1,22 @@
|
||||
#!/bin/bash
|
||||
pr_number="${1}"
|
||||
pr_number=$1
|
||||
|
||||
if [[ -z "${pr_number}" ]]; then
|
||||
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 || true)"
|
||||
if [[ -z "${base_dir}" ]]; then
|
||||
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"
|
||||
log_dir="$base_dir/.gemini/tmp/async-reviews/pr-$pr_number/logs"
|
||||
|
||||
if [[ ! -d "${log_dir}" ]]; then
|
||||
if [[ ! -d "$log_dir" ]]; then
|
||||
echo "STATUS: NOT_FOUND"
|
||||
echo "❌ No logs found for PR #${pr_number} in ${log_dir}"
|
||||
echo "❌ No logs found for PR #$pr_number in $log_dir"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -34,32 +34,32 @@ all_done=true
|
||||
echo "STATUS: CHECKING"
|
||||
|
||||
for task_info in "${tasks[@]}"; do
|
||||
IFS="|" read -r task_name log_file <<< "${task_info}"
|
||||
IFS="|" read -r task_name log_file <<< "$task_info"
|
||||
|
||||
file_path="${log_dir}/${log_file}"
|
||||
exit_file="${log_dir}/${task_name}.exit"
|
||||
file_path="$log_dir/$log_file"
|
||||
exit_file="$log_dir/$task_name.exit"
|
||||
|
||||
if [[ -f "${exit_file}" ]]; then
|
||||
read -r exit_code < "${exit_file}" || exit_code=""
|
||||
if [[ "${exit_code}" == "0" ]]; then
|
||||
echo "✅ ${task_name}: SUCCESS"
|
||||
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/^/ /' || true
|
||||
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"
|
||||
elif [[ -f "$file_path" ]]; then
|
||||
echo "⏳ $task_name: RUNNING"
|
||||
all_done=false
|
||||
else
|
||||
echo "➖ ${task_name}: NOT STARTED"
|
||||
echo "➖ $task_name: NOT STARTED"
|
||||
all_done=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${all_done}" == "true" ]]; then
|
||||
if $all_done; then
|
||||
echo "STATUS: COMPLETE"
|
||||
echo "LOG_DIR: ${log_dir}"
|
||||
echo "LOG_DIR: $log_dir"
|
||||
else
|
||||
echo "STATUS: IN_PROGRESS"
|
||||
fi
|
||||
fi
|
||||
@@ -175,9 +175,7 @@ runs:
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_CORE_PACKAGE_NAME}" \
|
||||
--no-tag
|
||||
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
|
||||
npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} false
|
||||
fi
|
||||
npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} false
|
||||
|
||||
- name: '🔗 Install latest core package'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
@@ -195,7 +193,7 @@ runs:
|
||||
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
|
||||
|
||||
- name: '📦 Prepare bundled CLI for npm release'
|
||||
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'"
|
||||
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/' && inputs.npm-tag != 'latest'"
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
@@ -250,9 +248,7 @@ runs:
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \
|
||||
--no-tag
|
||||
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
|
||||
npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} false
|
||||
fi
|
||||
npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} false
|
||||
|
||||
- name: '🔬 Verify NPM release by version'
|
||||
uses: './.github/actions/verify-release'
|
||||
@@ -291,25 +287,8 @@ runs:
|
||||
INPUTS_PREVIOUS_TAG: '${{ inputs.previous-tag }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
rm -f gemini-cli-bundle.zip
|
||||
(cd bundle && chmod +x gemini.js && zip -r ../gemini-cli-bundle.zip .)
|
||||
|
||||
echo "Testing the generated bundle archive..."
|
||||
rm -rf test-bundle
|
||||
mkdir -p test-bundle
|
||||
unzip -q gemini-cli-bundle.zip -d test-bundle
|
||||
|
||||
# Verify it runs and outputs a version
|
||||
BUNDLE_VERSION=$(node test-bundle/gemini.js --version | xargs)
|
||||
echo "Bundle version output: ${BUNDLE_VERSION}"
|
||||
if [[ -z "${BUNDLE_VERSION}" ]]; then
|
||||
echo "Error: Bundle failed to execute or return version."
|
||||
exit 1
|
||||
fi
|
||||
rm -rf test-bundle
|
||||
|
||||
gh release create "${INPUTS_RELEASE_TAG}" \
|
||||
gemini-cli-bundle.zip \
|
||||
bundle/gemini.js \
|
||||
--target "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}" \
|
||||
--title "Release ${INPUTS_RELEASE_TAG}" \
|
||||
--notes-start-tag "${INPUTS_PREVIOUS_TAG}" \
|
||||
|
||||
@@ -18,13 +18,6 @@ runs:
|
||||
env:
|
||||
JSON_INPUTS: '${{ toJSON(inputs) }}'
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
- name: 'Install system dependencies'
|
||||
if: "runner.os == 'Linux'"
|
||||
run: |
|
||||
sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap
|
||||
# Ubuntu 24.04+ requires this to allow bwrap to function in CI
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
shell: 'bash'
|
||||
- name: 'Run Tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
name: 'Evals: PR Guidance'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/core/src/**/*.ts'
|
||||
- '!**/*.test.ts'
|
||||
- '!**/*.test.tsx'
|
||||
|
||||
permissions:
|
||||
pull-requests: 'write'
|
||||
contents: 'read'
|
||||
|
||||
jobs:
|
||||
provide-guidance:
|
||||
name: 'Model Steering Guidance'
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Detect Steering Changes'
|
||||
id: 'detect'
|
||||
run: |
|
||||
STEERING_DETECTED=$(node scripts/changed_prompt.js --steering-only)
|
||||
echo "STEERING_DETECTED=$STEERING_DETECTED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Analyze PR Content'
|
||||
if: "steps.detect.outputs.STEERING_DETECTED == 'true'"
|
||||
id: 'analysis'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
# Check for behavioral eval changes
|
||||
EVAL_CHANGES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep "^evals/" || true)
|
||||
if [ -z "$EVAL_CHANGES" ]; then
|
||||
echo "MISSING_EVALS=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Check if user is a maintainer (has write/admin access)
|
||||
USER_PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission --jq '.permission')
|
||||
if [[ "$USER_PERMISSION" == "admin" || "$USER_PERMISSION" == "write" ]]; then
|
||||
echo "IS_MAINTAINER=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: 'Post Guidance Comment'
|
||||
if: "steps.detect.outputs.STEERING_DETECTED == 'true'"
|
||||
uses: 'thollander/actions-comment-pull-request@65f9e5c9a1f2cd378bd74b2e057c9736982a8e74' # ratchet:thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
comment-tag: 'eval-guidance-bot'
|
||||
message: |
|
||||
### 🧠 Model Steering Guidance
|
||||
|
||||
This PR modifies files that affect the model's behavior (prompts, tools, or instructions).
|
||||
|
||||
${{ steps.analysis.outputs.MISSING_EVALS == 'true' && '- ⚠️ **Consider adding Evals:** No behavioral evaluations (`evals/*.eval.ts`) were added or updated in this PR. Consider adding a test case to verify the new behavior and prevent regressions.' || '' }}
|
||||
${{ steps.analysis.outputs.IS_MAINTAINER == 'true' && '- 🚀 **Maintainer Reminder:** Please ensure that these changes do not regress results on benchmark evals before merging.' || '' }}
|
||||
|
||||
---
|
||||
*This is an automated guidance message triggered by steering logic signatures.*
|
||||
@@ -1,137 +0,0 @@
|
||||
name: 'Evals: PR Evaluation & Regression'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: ['opened', 'synchronize', 'reopened', 'ready_for_review']
|
||||
paths:
|
||||
- 'packages/core/src/prompts/**'
|
||||
- 'packages/core/src/tools/**'
|
||||
- 'packages/core/src/agents/**'
|
||||
- 'evals/**'
|
||||
- '!**/*.test.ts'
|
||||
- '!**/*.test.tsx'
|
||||
workflow_dispatch:
|
||||
|
||||
# Prevents multiple runs for the same PR simultaneously (saves tokens)
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
pull-requests: 'write'
|
||||
contents: 'read'
|
||||
actions: 'read'
|
||||
|
||||
jobs:
|
||||
pr-evaluation:
|
||||
name: 'Evaluate Steering & Regressions'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository))"
|
||||
# External contributors' PRs will wait for approval in this environment
|
||||
environment: |-
|
||||
${{ (github.event.pull_request.head.repo.full_name == github.repository) && 'internal' || 'external-evals' }}
|
||||
env:
|
||||
# CENTRALIZED MODEL LIST
|
||||
MODEL_LIST: 'gemini-3-flash-preview'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Detect Steering Changes'
|
||||
id: 'detect'
|
||||
run: |
|
||||
SHOULD_RUN=$(node scripts/changed_prompt.js)
|
||||
STEERING_DETECTED=$(node scripts/changed_prompt.js --steering-only)
|
||||
echo "SHOULD_RUN=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
|
||||
echo "STEERING_DETECTED=$STEERING_DETECTED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Analyze PR Content (Guidance)'
|
||||
if: "steps.detect.outputs.STEERING_DETECTED == 'true'"
|
||||
id: 'analysis'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
# Check for behavioral eval changes
|
||||
EVAL_CHANGES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep "^evals/" || true)
|
||||
if [ -z "$EVAL_CHANGES" ]; then
|
||||
echo "MISSING_EVALS=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Check if user is a maintainer
|
||||
USER_PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission --jq '.permission')
|
||||
if [[ "$USER_PERMISSION" == "admin" || "$USER_PERMISSION" == "write" ]]; then
|
||||
echo "IS_MAINTAINER=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: 'Execute Regression Check'
|
||||
if: "steps.detect.outputs.SHOULD_RUN == 'true'"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
MODEL_LIST: '${{ env.MODEL_LIST }}'
|
||||
run: |
|
||||
# Run the regression check loop. The script saves the report to a file.
|
||||
node scripts/run_eval_regression.js
|
||||
|
||||
# Use the generated report file if it exists
|
||||
if [[ -f eval_regression_report.md ]]; then
|
||||
echo "REPORT_FILE=eval_regression_report.md" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: 'Post or Update PR Comment'
|
||||
if: "always() && steps.detect.outputs.STEERING_DETECTED == 'true'"
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
# 1. Build the full comment body
|
||||
{
|
||||
if [[ -f eval_regression_report.md ]]; then
|
||||
cat eval_regression_report.md
|
||||
echo ""
|
||||
fi
|
||||
echo "### 🧠 Model Steering Guidance"
|
||||
echo ""
|
||||
echo "This PR modifies files that affect the model's behavior (prompts, tools, or instructions)."
|
||||
echo ""
|
||||
|
||||
if [[ "${{ steps.analysis.outputs.MISSING_EVALS }}" == "true" ]]; then
|
||||
echo "- ⚠️ **Consider adding Evals:** No behavioral evaluations (\`evals/*.eval.ts\`) were added or updated in this PR. Consider [adding a test case](https://github.com/google-gemini/gemini-cli/blob/main/evals/README.md#creating-an-evaluation) to verify the new behavior and prevent regressions."
|
||||
fi
|
||||
|
||||
if [[ "${{ steps.analysis.outputs.IS_MAINTAINER }}" == "true" ]]; then
|
||||
echo "- 🚀 **Maintainer Reminder:** Please ensure that these changes do not regress results on benchmark evals before merging."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "---"
|
||||
echo "*This is an automated guidance message triggered by steering logic signatures.*"
|
||||
echo "<!-- eval-pr-report -->"
|
||||
} > full_comment.md
|
||||
|
||||
# 2. Find if a comment with our unique tag already exists
|
||||
# We extract the numeric ID from the URL to ensure compatibility with the REST API
|
||||
COMMENT_ID=$(gh pr view ${{ github.event.pull_request.number }} --json comments --jq '.comments[] | select(.body | contains("<!-- eval-pr-report -->")) | .url' | grep -oE "[0-9]+$" | head -n 1)
|
||||
|
||||
# 3. Update or Create the comment
|
||||
if [ -n "$COMMENT_ID" ]; then
|
||||
echo "Updating existing comment $COMMENT_ID via API..."
|
||||
gh api -X PATCH "repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -F body=@full_comment.md
|
||||
else
|
||||
echo "Creating new PR comment..."
|
||||
gh pr comment ${{ github.event.pull_request.number }} --body-file full_comment.md
|
||||
fi
|
||||
+5
-8
@@ -346,11 +346,9 @@ npm run lint
|
||||
|
||||
- Please adhere to the coding style, patterns, and conventions used throughout
|
||||
the existing codebase.
|
||||
- Consult
|
||||
[GEMINI.md](https://github.com/google-gemini/gemini-cli/blob/main/GEMINI.md)
|
||||
(typically found in the project root) for specific instructions related to
|
||||
AI-assisted development, including conventions for React, comments, and Git
|
||||
usage.
|
||||
- Consult [GEMINI.md](../GEMINI.md) (typically found in the project root) for
|
||||
specific instructions related to AI-assisted development, including
|
||||
conventions for React, comments, and Git usage.
|
||||
- **Imports:** Pay special attention to import paths. The project uses ESLint to
|
||||
enforce restrictions on relative imports between packages.
|
||||
|
||||
@@ -507,9 +505,8 @@ code.
|
||||
|
||||
### Documentation structure
|
||||
|
||||
Our documentation is organized using
|
||||
[sidebar.json](https://github.com/google-gemini/gemini-cli/blob/main/docs/sidebar.json)
|
||||
as the table of contents. When adding new documentation:
|
||||
Our documentation is organized using [sidebar.json](/docs/sidebar.json) as the
|
||||
table of contents. When adding new documentation:
|
||||
|
||||
1. Create your markdown file **in the appropriate directory** under `/docs`.
|
||||
2. Add an entry to `sidebar.json` in the relevant section.
|
||||
|
||||
@@ -30,7 +30,7 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
|
||||
## 📦 Installation
|
||||
|
||||
See
|
||||
[Gemini CLI installation, execution, and releases](https://www.geminicli.com/docs/get-started/installation)
|
||||
[Gemini CLI installation, execution, and releases](./docs/get-started/installation.md)
|
||||
for recommended system specifications and a detailed installation guide.
|
||||
|
||||
### Quick Install
|
||||
@@ -71,9 +71,9 @@ conda activate gemini_env
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
|
||||
## Release Channels
|
||||
## Release Cadence and Tags
|
||||
|
||||
See [Releases](https://www.geminicli.com/docs/changelogs) for more details.
|
||||
See [Releases](./docs/releases.md) for more details.
|
||||
|
||||
### Preview
|
||||
|
||||
@@ -209,7 +209,7 @@ gemini
|
||||
```
|
||||
|
||||
For Google Workspace accounts and other authentication methods, see the
|
||||
[authentication guide](https://www.geminicli.com/docs/get-started/authentication).
|
||||
[authentication guide](./docs/get-started/authentication.md).
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
@@ -278,64 +278,59 @@ gemini
|
||||
|
||||
### Getting Started
|
||||
|
||||
- [**Quickstart Guide**](https://www.geminicli.com/docs/get-started) - Get up
|
||||
and running quickly.
|
||||
- [**Authentication Setup**](https://www.geminicli.com/docs/get-started/authentication) -
|
||||
Detailed auth configuration.
|
||||
- [**Configuration Guide**](https://www.geminicli.com/docs/reference/configuration) -
|
||||
Settings and customization.
|
||||
- [**Keyboard Shortcuts**](https://www.geminicli.com/docs/reference/keyboard-shortcuts) -
|
||||
- [**Quickstart Guide**](./docs/get-started/index.md) - Get up and running
|
||||
quickly.
|
||||
- [**Authentication Setup**](./docs/get-started/authentication.md) - Detailed
|
||||
auth configuration.
|
||||
- [**Configuration Guide**](./docs/reference/configuration.md) - Settings and
|
||||
customization.
|
||||
- [**Keyboard Shortcuts**](./docs/reference/keyboard-shortcuts.md) -
|
||||
Productivity tips.
|
||||
|
||||
### Core Features
|
||||
|
||||
- [**Commands Reference**](https://www.geminicli.com/docs/reference/commands) -
|
||||
All slash commands (`/help`, `/chat`, etc).
|
||||
- [**Custom Commands**](https://www.geminicli.com/docs/cli/custom-commands) -
|
||||
Create your own reusable commands.
|
||||
- [**Context Files (GEMINI.md)**](https://www.geminicli.com/docs/cli/gemini-md) -
|
||||
Provide persistent context to Gemini CLI.
|
||||
- [**Checkpointing**](https://www.geminicli.com/docs/cli/checkpointing) - Save
|
||||
and resume conversations.
|
||||
- [**Token Caching**](https://www.geminicli.com/docs/cli/token-caching) -
|
||||
Optimize token usage.
|
||||
- [**Commands Reference**](./docs/reference/commands.md) - All slash commands
|
||||
(`/help`, `/chat`, etc).
|
||||
- [**Custom Commands**](./docs/cli/custom-commands.md) - Create your own
|
||||
reusable commands.
|
||||
- [**Context Files (GEMINI.md)**](./docs/cli/gemini-md.md) - Provide persistent
|
||||
context to Gemini CLI.
|
||||
- [**Checkpointing**](./docs/cli/checkpointing.md) - Save and resume
|
||||
conversations.
|
||||
- [**Token Caching**](./docs/cli/token-caching.md) - Optimize token usage.
|
||||
|
||||
### Tools & Extensions
|
||||
|
||||
- [**Built-in Tools Overview**](https://www.geminicli.com/docs/reference/tools)
|
||||
- [File System Operations](https://www.geminicli.com/docs/tools/file-system)
|
||||
- [Shell Commands](https://www.geminicli.com/docs/tools/shell)
|
||||
- [Web Fetch & Search](https://www.geminicli.com/docs/tools/web-fetch)
|
||||
- [**MCP Server Integration**](https://www.geminicli.com/docs/tools/mcp-server) -
|
||||
Extend with custom tools.
|
||||
- [**Custom Extensions**](https://geminicli.com/docs/extensions/writing-extensions) -
|
||||
Build and share your own commands.
|
||||
- [**Built-in Tools Overview**](./docs/reference/tools.md)
|
||||
- [File System Operations](./docs/tools/file-system.md)
|
||||
- [Shell Commands](./docs/tools/shell.md)
|
||||
- [Web Fetch & Search](./docs/tools/web-fetch.md)
|
||||
- [**MCP Server Integration**](./docs/tools/mcp-server.md) - Extend with custom
|
||||
tools.
|
||||
- [**Custom Extensions**](./docs/extensions/index.md) - Build and share your own
|
||||
commands.
|
||||
|
||||
### Advanced Topics
|
||||
|
||||
- [**Headless Mode (Scripting)**](https://www.geminicli.com/docs/cli/headless) -
|
||||
Use Gemini CLI in automated workflows.
|
||||
- [**IDE Integration**](https://www.geminicli.com/docs/ide-integration) - VS
|
||||
Code companion.
|
||||
- [**Sandboxing & Security**](https://www.geminicli.com/docs/cli/sandbox) - Safe
|
||||
execution environments.
|
||||
- [**Trusted Folders**](https://www.geminicli.com/docs/cli/trusted-folders) -
|
||||
Control execution policies by folder.
|
||||
- [**Enterprise Guide**](https://www.geminicli.com/docs/cli/enterprise) - Deploy
|
||||
and manage in a corporate environment.
|
||||
- [**Telemetry & Monitoring**](https://www.geminicli.com/docs/cli/telemetry) -
|
||||
Usage tracking.
|
||||
- [**Tools reference**](https://www.geminicli.com/docs/reference/tools) -
|
||||
Built-in tools overview.
|
||||
- [**Local development**](https://www.geminicli.com/docs/local-development) -
|
||||
Local development tooling.
|
||||
- [**Headless Mode (Scripting)**](./docs/cli/headless.md) - Use Gemini CLI in
|
||||
automated workflows.
|
||||
- [**IDE Integration**](./docs/ide-integration/index.md) - VS Code companion.
|
||||
- [**Sandboxing & Security**](./docs/cli/sandbox.md) - Safe execution
|
||||
environments.
|
||||
- [**Trusted Folders**](./docs/cli/trusted-folders.md) - Control execution
|
||||
policies by folder.
|
||||
- [**Enterprise Guide**](./docs/cli/enterprise.md) - Deploy and manage in a
|
||||
corporate environment.
|
||||
- [**Telemetry & Monitoring**](./docs/cli/telemetry.md) - Usage tracking.
|
||||
- [**Tools reference**](./docs/reference/tools.md) - Built-in tools overview.
|
||||
- [**Local development**](./docs/local-development.md) - Local development
|
||||
tooling.
|
||||
|
||||
### Troubleshooting & Support
|
||||
|
||||
- [**Troubleshooting Guide**](https://www.geminicli.com/docs/resources/troubleshooting) -
|
||||
Common issues and solutions.
|
||||
- [**FAQ**](https://www.geminicli.com/docs/resources/faq) - Frequently asked
|
||||
questions.
|
||||
- [**Troubleshooting Guide**](./docs/resources/troubleshooting.md) - Common
|
||||
issues and solutions.
|
||||
- [**FAQ**](./docs/resources/faq.md) - Frequently asked questions.
|
||||
- Use `/bug` command to report issues directly from the CLI.
|
||||
|
||||
### Using MCP Servers
|
||||
@@ -349,9 +344,8 @@ custom tools:
|
||||
> @database Run a query to find inactive users
|
||||
```
|
||||
|
||||
See the
|
||||
[MCP Server Integration guide](https://www.geminicli.com/docs/tools/mcp-server)
|
||||
for setup instructions.
|
||||
See the [MCP Server Integration guide](./docs/tools/mcp-server.md) for setup
|
||||
instructions.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
@@ -372,8 +366,7 @@ for planned features and priorities.
|
||||
## 📖 Resources
|
||||
|
||||
- **[Official Roadmap](./ROADMAP.md)** - See what's coming next.
|
||||
- **[Changelog](https://www.geminicli.com/docs/changelogs)** - See recent
|
||||
notable updates.
|
||||
- **[Changelog](./docs/changelogs/index.md)** - See recent notable updates.
|
||||
- **[NPM Package](https://www.npmjs.com/package/@google/gemini-cli)** - Package
|
||||
registry.
|
||||
- **[GitHub Issues](https://github.com/google-gemini/gemini-cli/issues)** -
|
||||
@@ -383,14 +376,13 @@ for planned features and priorities.
|
||||
|
||||
### Uninstall
|
||||
|
||||
See the [Uninstall Guide](https://www.geminicli.com/docs/resources/uninstall)
|
||||
for removal instructions.
|
||||
See the [Uninstall Guide](./docs/resources/uninstall.md) for removal
|
||||
instructions.
|
||||
|
||||
## 📄 Legal
|
||||
|
||||
- **License**: [Apache License 2.0](LICENSE)
|
||||
- **Terms of Service**:
|
||||
[Terms & Privacy](https://www.geminicli.com/docs/resources/tos-privacy)
|
||||
- **Terms of Service**: [Terms & Privacy](./docs/resources/tos-privacy.md)
|
||||
- **Security**: [Security Policy](SECURITY.md)
|
||||
|
||||
---
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB |
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.35.3
|
||||
# Latest stable release: v0.35.2
|
||||
|
||||
Released: March 28, 2026
|
||||
Released: March 26, 2026
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -29,9 +29,6 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix(patch): cherry-pick 765fb67 to release/v0.35.2-pr-24055 [CONFLICTS] by
|
||||
@gemini-cli-robot in
|
||||
[#24063](https://github.com/google-gemini/gemini-cli/pull/24063)
|
||||
- fix(core): allow disabling environment variable redaction by @galz10 in
|
||||
[#23927](https://github.com/google-gemini/gemini-cli/pull/23927)
|
||||
- fix(a2a-server): A2A server should execute ask policies in interactive mode by
|
||||
@@ -388,4 +385,4 @@ npm install -g @google/gemini-cli
|
||||
[#23585](https://github.com/google-gemini/gemini-cli/pull/23585)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.34.0...v0.35.3
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.34.0...v0.35.2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Preview release: v0.36.0-preview.8
|
||||
# Preview release: v0.36.0-preview.5
|
||||
|
||||
Released: April 01, 2026
|
||||
Released: March 27, 2026
|
||||
|
||||
Our preview release includes the latest, new, and experimental features. This
|
||||
release may not be as stable as our [latest weekly release](latest.md).
|
||||
@@ -31,10 +31,6 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix(patch): cherry-pick 765fb67 to release/v0.36.0-preview.5-pr-24055 to patch
|
||||
version v0.36.0-preview.5 and create version 0.36.0-preview.6 by
|
||||
@gemini-cli-robot in
|
||||
[#24061](https://github.com/google-gemini/gemini-cli/pull/24061)
|
||||
- fix(a2a-server): A2A server should execute ask policies in interactive mode by
|
||||
@kschaab in [#23831](https://github.com/google-gemini/gemini-cli/pull/23831)
|
||||
- docs(core): document agent_card_json string literal options for remote agents
|
||||
@@ -390,4 +386,4 @@ npm install -g @google/gemini-cli@preview
|
||||
[#23666](https://github.com/google-gemini/gemini-cli/pull/23666)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.35.0-preview.5...v0.36.0-preview.8
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.35.0-preview.5...v0.36.0-preview.5
|
||||
|
||||
@@ -52,7 +52,7 @@ These commands are available within the interactive REPL.
|
||||
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
|
||||
| `--worktree` | `-w` | string | - | Start Gemini in a new git worktree. If no name is provided, one is generated automatically. Requires `experimental.worktrees: true` in settings. |
|
||||
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
|
||||
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo`, `plan` |
|
||||
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
|
||||
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
|
||||
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
|
||||
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
|
||||
|
||||
+8
-13
@@ -39,9 +39,7 @@ To start Plan Mode while using Gemini CLI:
|
||||
the rotation when Gemini CLI is actively processing or showing confirmation
|
||||
dialogs.
|
||||
|
||||
- **Command:** Type `/plan [goal]` in the input box. The `[goal]` is optional;
|
||||
for example, `/plan implement authentication` will switch to Plan Mode and
|
||||
immediately submit the prompt to the model.
|
||||
- **Command:** Type `/plan` in the input box.
|
||||
|
||||
- **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI
|
||||
calls the
|
||||
@@ -56,21 +54,19 @@ Gemini CLI takes action.
|
||||
|
||||
1. **Provide a goal:** Start by describing what you want to achieve. Gemini CLI
|
||||
will then enter Plan Mode (if it's not already) to research the task.
|
||||
2. **Discuss and agree on strategy:** As Gemini CLI analyzes your codebase, it
|
||||
will discuss its findings and proposed strategy with you to ensure
|
||||
alignment. It may ask you questions or present different implementation
|
||||
options using [`ask_user`](../tools/ask-user.md). **Gemini CLI will stop and
|
||||
wait for your confirmation** before drafting the formal plan. You should
|
||||
reach an informal agreement on the approach before proceeding.
|
||||
3. **Review the plan:** Once you've agreed on the strategy, Gemini CLI creates
|
||||
a detailed implementation plan as a Markdown file in your plans directory.
|
||||
2. **Review research and provide input:** As Gemini CLI analyzes your codebase,
|
||||
it may ask you questions or present different implementation options using
|
||||
[`ask_user`](../tools/ask-user.md). Provide your preferences to help guide
|
||||
the design.
|
||||
3. **Review the plan:** Once Gemini CLI has a proposed strategy, it creates a
|
||||
detailed implementation plan as a Markdown file in your plans directory.
|
||||
- **View:** You can open and read this file to understand the proposed
|
||||
changes.
|
||||
- **Edit:** Press `Ctrl+X` to open the plan directly in your configured
|
||||
external editor.
|
||||
|
||||
4. **Approve or iterate:** Gemini CLI will present the finalized plan for your
|
||||
formal approval.
|
||||
approval.
|
||||
- **Approve:** If you're satisfied with the plan, approve it to start the
|
||||
implementation immediately: **Yes, automatically accept edits** or **Yes,
|
||||
manually accept edits**.
|
||||
@@ -123,7 +119,6 @@ These are the only allowed tools:
|
||||
[`glob`](../tools/file-system.md#4-glob-findfiles)
|
||||
- **Search:** [`grep_search`](../tools/file-system.md#5-grep_search-searchtext),
|
||||
[`google_web_search`](../tools/web-search.md),
|
||||
[`web_fetch`](../tools/web-fetch.md) (requires explicit confirmation),
|
||||
[`get_internal_docs`](../tools/internal-docs.md)
|
||||
- **Research Subagents:**
|
||||
[`codebase_investigator`](../core/subagents.md#codebase-investigator),
|
||||
|
||||
+48
-45
@@ -30,7 +30,6 @@ they appear in the UI.
|
||||
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. YOLO mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo). | `"default"` |
|
||||
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
|
||||
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. | `false` |
|
||||
| Enable Plan Mode | `general.plan.enabled` | Enable Plan Mode for read-only safety during planning. | `true` |
|
||||
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode. | `undefined` |
|
||||
| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` |
|
||||
| Retry Fetch Errors | `general.retryFetchErrors` | Retry on "exception TypeError: fetch failed sending request" errors. | `true` |
|
||||
@@ -47,39 +46,38 @@ they appear in the UI.
|
||||
|
||||
### UI
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
|
||||
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
|
||||
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
|
||||
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
|
||||
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
|
||||
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
|
||||
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
|
||||
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
|
||||
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
|
||||
| Escape Pasted @ Symbols | `ui.escapePastedAtSymbols` | When enabled, @ symbols in pasted text are escaped to prevent unintended @path expansion. | `false` |
|
||||
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
|
||||
| Compact Tool Output | `ui.compactToolOutput` | Display tool outputs (like directory listings and file reads) in a compact, structured format. | `false` |
|
||||
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
|
||||
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
|
||||
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory in the footer. | `false` |
|
||||
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
|
||||
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
|
||||
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window usage percentage. | `true` |
|
||||
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
|
||||
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
|
||||
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
|
||||
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
|
||||
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
|
||||
| Show User Identity | `ui.showUserIdentity` | Show the signed-in user's identity (e.g. email) in the UI. | `true` |
|
||||
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
|
||||
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
|
||||
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
||||
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
|
||||
| Loading Phrases | `ui.loadingPhrases` | What to show while the model is working: tips, witty comments, all, or off. | `"off"` |
|
||||
| Error Verbosity | `ui.errorVerbosity` | Controls whether recoverable errors are hidden (low) or fully shown (full). | `"low"` |
|
||||
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
||||
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
|
||||
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
|
||||
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
|
||||
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
|
||||
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
|
||||
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
|
||||
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
|
||||
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
|
||||
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
|
||||
| Escape Pasted @ Symbols | `ui.escapePastedAtSymbols` | When enabled, @ symbols in pasted text are escaped to prevent unintended @path expansion. | `false` |
|
||||
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
|
||||
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
|
||||
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
|
||||
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory in the footer. | `false` |
|
||||
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
|
||||
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
|
||||
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window usage percentage. | `true` |
|
||||
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
|
||||
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
|
||||
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
|
||||
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
|
||||
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
|
||||
| Show User Identity | `ui.showUserIdentity` | Show the signed-in user's identity (e.g. email) in the UI. | `true` |
|
||||
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
|
||||
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
|
||||
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
||||
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
|
||||
| Loading Phrases | `ui.loadingPhrases` | What to show while the model is working: tips, witty comments, both, or nothing. | `"tips"` |
|
||||
| Error Verbosity | `ui.errorVerbosity` | Controls whether recoverable errors are hidden (low) or fully shown (full). | `"low"` |
|
||||
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
|
||||
|
||||
### IDE
|
||||
|
||||
@@ -153,20 +151,25 @@ they appear in the UI.
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| --------------------------------- | ------------------------------ | --------------------------------------------- | ------- |
|
||||
| Auto Configure Max Old Space Size | `advanced.autoConfigureMemory` | Automatically configure Node.js memory limits | `true` |
|
||||
| Auto Configure Max Old Space Size | `advanced.autoConfigureMemory` | Automatically configure Node.js memory limits | `false` |
|
||||
|
||||
### Experimental
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
|
||||
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
|
||||
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
|
||||
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ---------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
|
||||
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| 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` |
|
||||
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
|
||||
| Agent History Truncation | `experimental.agentHistoryTruncation` | Enable truncation window logic for the Agent History Provider. | `false` |
|
||||
| Agent History Truncation Threshold | `experimental.agentHistoryTruncationThreshold` | The maximum number of messages before history is truncated. | `30` |
|
||||
| Agent History Retained Messages | `experimental.agentHistoryRetainedMessages` | The number of recent messages to retain after truncation. | `15` |
|
||||
| Agent History Summarization | `experimental.agentHistorySummarization` | Enable summarization of truncated content via a small model for the Agent History Provider. | `false` |
|
||||
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
|
||||
|
||||
### Skills
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ using the `/theme` command within Gemini CLI:
|
||||
- `Holiday`
|
||||
- `Shades Of Purple`
|
||||
- `Solarized Dark`
|
||||
- `Tokyo Night`
|
||||
- **Light themes:**
|
||||
- `ANSI Light`
|
||||
- `Ayu Light`
|
||||
@@ -253,10 +252,6 @@ identify their source, for example: `shades-of-green (green-extension)`.
|
||||
|
||||
<img src="/docs/assets/theme-solarized-dark.png" alt="Solarized Dark theme" width="600">
|
||||
|
||||
### Tokyo Night
|
||||
|
||||
<img src="/docs/assets/theme-tokyonight-dark.png" alt="Tokyo Night theme" width="600">
|
||||
|
||||
## Light themes
|
||||
|
||||
### ANSI Light
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ requests sent from `packages/cli`. For a general overview of Gemini CLI, see the
|
||||
|
||||
## Navigating this section
|
||||
|
||||
- **[Sub-agents](./subagents.md):** Learn how to create and use specialized
|
||||
sub-agents for complex tasks.
|
||||
- **[Sub-agents (experimental)](./subagents.md):** Learn how to create and use
|
||||
specialized sub-agents for complex tasks.
|
||||
- **[Core tools reference](../reference/tools.md):** Information on how tools
|
||||
are defined, registered, and used by the core.
|
||||
- **[Memory Import Processor](../reference/memport.md):** Documentation for the
|
||||
|
||||
+23
-134
@@ -1,16 +1,20 @@
|
||||
# Subagents
|
||||
# Subagents (experimental)
|
||||
|
||||
Subagents are specialized agents that operate within your main Gemini CLI
|
||||
session. They are designed to handle specific, complex tasks—like deep codebase
|
||||
analysis, documentation lookup, or domain-specific reasoning—without cluttering
|
||||
the main agent's context or toolset.
|
||||
|
||||
Subagents are enabled by default. To disable them, set `enableAgents` to `false`
|
||||
in your `settings.json`:
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Subagents are currently an experimental feature.
|
||||
>
|
||||
To use custom subagents, you must ensure they are enabled in your
|
||||
`settings.json` (enabled by default):
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": { "enableAgents": false }
|
||||
"experimental": { "enableAgents": true }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -222,65 +226,19 @@ the `click_at` tool for precise, coordinate-based interactions.
|
||||
> The visual agent requires API key or Vertex AI authentication. It is
|
||||
> not available when using "Sign in with Google".
|
||||
|
||||
#### Sandbox support
|
||||
|
||||
The browser agent adjusts its behavior automatically when running inside a
|
||||
sandbox.
|
||||
|
||||
##### macOS seatbelt (`sandbox-exec`)
|
||||
|
||||
When the CLI runs under the macOS seatbelt sandbox, `persistent` and `isolated`
|
||||
session modes are forced to `isolated` with `headless` enabled. This avoids
|
||||
permission errors caused by seatbelt file-system restrictions on persistent
|
||||
browser profiles. If `sessionMode` is set to `existing`, no override is applied.
|
||||
|
||||
##### Container sandboxes (Docker / Podman)
|
||||
|
||||
Chrome is not available inside the container, so the browser agent is
|
||||
**disabled** unless `sessionMode` is set to `"existing"`. When enabled with
|
||||
`existing` mode, the agent automatically connects to Chrome on the host via the
|
||||
resolved IP of `host.docker.internal:9222` instead of using local pipe
|
||||
discovery. Port `9222` is currently hardcoded and cannot be customized.
|
||||
|
||||
To use the browser agent in a Docker sandbox:
|
||||
|
||||
1. Start Chrome on the host with remote debugging enabled:
|
||||
|
||||
```bash
|
||||
# Option A: Launch Chrome from the command line
|
||||
google-chrome --remote-debugging-port=9222
|
||||
|
||||
# Option B: Enable in Chrome settings
|
||||
# Navigate to chrome://inspect/#remote-debugging and enable
|
||||
```
|
||||
|
||||
2. Configure `sessionMode` and allowed domains in your project's
|
||||
`.gemini/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"overrides": {
|
||||
"browser_agent": { "enabled": true }
|
||||
},
|
||||
"browser": {
|
||||
"sessionMode": "existing",
|
||||
"allowedDomains": ["example.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Launch the CLI with port forwarding:
|
||||
|
||||
```bash
|
||||
GEMINI_SANDBOX=docker SANDBOX_PORTS=9222 gemini
|
||||
```
|
||||
|
||||
## Creating custom subagents
|
||||
|
||||
You can create your own subagents to automate specific workflows or enforce
|
||||
specific personas.
|
||||
specific personas. To use custom subagents, you must enable them in your
|
||||
`settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"enableAgents": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Agent definition files
|
||||
|
||||
@@ -332,7 +290,6 @@ it yourself; just report it.
|
||||
| `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.** |
|
||||
| `mcpServers` | object | No | Configuration for inline Model Context Protocol (MCP) servers isolated to this specific agent. |
|
||||
| `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`. |
|
||||
@@ -360,78 +317,6 @@ Each subagent runs in its own isolated context loop. This means:
|
||||
subagents **cannot** call other subagents. If a subagent is granted the `*`
|
||||
tool wildcard, it will still be unable to see or invoke other agents.
|
||||
|
||||
## Subagent tool isolation
|
||||
|
||||
Subagent tool isolation moves Gemini CLI away from a single global tool
|
||||
registry. By providing isolated execution environments, you can ensure that
|
||||
subagents only interact with the parts of the system they are designed for. This
|
||||
prevents unintended side effects, improves reliability by avoiding state
|
||||
contamination, and enables fine-grained permission control.
|
||||
|
||||
With this feature, you can:
|
||||
|
||||
- **Specify tool access:** Define exactly which tools an agent can access using
|
||||
a `tools` list in the agent definition.
|
||||
- **Define inline MCP servers:** Configure Model Context Protocol (MCP) servers
|
||||
(which provide a standardized way to connect AI models to external tools and
|
||||
data sources) directly in the subagent's markdown frontmatter, isolating them
|
||||
to that specific agent.
|
||||
- **Maintain state isolation:** Ensure that subagents only interact with their
|
||||
own set of tools and servers, preventing side effects and state contamination.
|
||||
- **Apply subagent-specific policies:** Enforce granular rules in your
|
||||
[Policy Engine](../reference/policy-engine.md) TOML configuration based on the
|
||||
executing subagent's name.
|
||||
|
||||
### Configuring isolated tools and servers
|
||||
|
||||
You can configure tool isolation for a subagent by updating its markdown
|
||||
frontmatter. This allows you to explicitly state which tools the subagent can
|
||||
use, rather than relying on the global registry.
|
||||
|
||||
Add an `mcpServers` object to define inline MCP servers that are unique to the
|
||||
agent.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: my-isolated-agent
|
||||
tools:
|
||||
- grep_search
|
||||
- read_file
|
||||
mcpServers:
|
||||
my-custom-server:
|
||||
command: 'node'
|
||||
args: ['path/to/server.js']
|
||||
---
|
||||
```
|
||||
|
||||
### Subagent-specific policies
|
||||
|
||||
You can enforce fine-grained control over subagents using the
|
||||
[Policy Engine's](../reference/policy-engine.md) TOML configuration. This allows
|
||||
you to grant or restrict permissions specifically for an agent, without
|
||||
affecting the rest of your CLI session.
|
||||
|
||||
To restrict a policy rule to a specific subagent, add the `subagent` property to
|
||||
the `[[rules]]` block in your `policy.toml` file.
|
||||
|
||||
**Example:**
|
||||
|
||||
```toml
|
||||
[[rules]]
|
||||
name = "Allow pr-creator to push code"
|
||||
subagent = "pr-creator"
|
||||
description = "Permit pr-creator to push branches automatically."
|
||||
action = "allow"
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "git push"
|
||||
```
|
||||
|
||||
In this configuration, the policy rule only triggers if the executing subagent's
|
||||
name matches `pr-creator`. Rules without the `subagent` property apply
|
||||
universally to all agents.
|
||||
|
||||
## Managing subagents
|
||||
|
||||
You can manage subagents interactively using the `/agents` command or
|
||||
@@ -521,11 +406,15 @@ If you need to further tune your subagent, you can do so by selecting the model
|
||||
to optimize for with `/model` and then asking the model why it does not think
|
||||
that your subagent was called with a specific prompt and the given description.
|
||||
|
||||
## Remote subagents (Agent2Agent)
|
||||
## Remote subagents (Agent2Agent) (experimental)
|
||||
|
||||
Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
|
||||
(A2A) protocol.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Remote subagents are currently an experimental feature.
|
||||
|
||||
See the [Remote Subagents documentation](remote-agents) for detailed
|
||||
configuration, authentication, and usage instructions.
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
### `/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,
|
||||
@@ -303,7 +305,7 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **Description:** Switch to Plan Mode (read-only) and view the current plan if
|
||||
one has been generated.
|
||||
- **Note:** This feature is enabled by default. It can be disabled via the
|
||||
`general.plan.enabled` setting in your configuration.
|
||||
`experimental.plan` setting in your configuration.
|
||||
- **Sub-commands:**
|
||||
- **`copy`**:
|
||||
- **Description:** Copy the currently approved plan to your clipboard.
|
||||
|
||||
@@ -141,11 +141,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`general.plan.enabled`** (boolean):
|
||||
- **Description:** Enable Plan Mode for read-only safety during planning.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`general.plan.directory`** (string):
|
||||
- **Description:** The directory where planning artifacts are stored. If not
|
||||
specified, defaults to the system temporary directory. A custom directory
|
||||
@@ -262,11 +257,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Show the "? for shortcuts" hint above the input.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`ui.compactToolOutput`** (boolean):
|
||||
- **Description:** Display tool outputs (like directory listings and file
|
||||
reads) in a compact, structured format.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.hideBanner`** (boolean):
|
||||
- **Description:** Hide the application banner
|
||||
- **Default:** `false`
|
||||
@@ -354,8 +344,8 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **`ui.loadingPhrases`** (enum):
|
||||
- **Description:** What to show while the model is working: tips, witty
|
||||
comments, all, or off.
|
||||
- **Default:** `"off"`
|
||||
comments, both, or nothing.
|
||||
- **Default:** `"tips"`
|
||||
- **Values:** `"tips"`, `"witty"`, `"all"`, `"off"`
|
||||
|
||||
- **`ui.errorVerbosity`** (enum):
|
||||
@@ -1376,14 +1366,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`tools.shell.backgroundCompletionBehavior`** (enum):
|
||||
- **Description:** Controls what happens when a background shell command
|
||||
finishes. 'silent' (default): quietly exits in background. 'inject':
|
||||
automatically returns output to agent. 'notify': shows brief message in
|
||||
chat.
|
||||
- **Default:** `"silent"`
|
||||
- **Values:** `"silent"`, `"inject"`, `"notify"`
|
||||
|
||||
- **`tools.shell.pager`** (string):
|
||||
- **Description:** The pager command to use for shell output. Defaults to
|
||||
`cat`.
|
||||
@@ -1565,7 +1547,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **`advanced.autoConfigureMemory`** (boolean):
|
||||
- **Description:** Automatically configure Node.js memory limits
|
||||
- **Default:** `true`
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`advanced.dnsResolutionOrder`** (string):
|
||||
@@ -1587,9 +1569,26 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
#### `experimental`
|
||||
|
||||
- **`experimental.adk.agentSessionNoninteractiveEnabled`** (boolean):
|
||||
- **Description:** Enable non-interactive agent sessions.
|
||||
- **Default:** `false`
|
||||
- **`experimental.toolOutputMasking.enabled`** (boolean):
|
||||
- **Description:** Enables tool output masking to save tokens.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.toolOutputMasking.toolProtectionThreshold`** (number):
|
||||
- **Description:** Minimum number of tokens to protect from masking (most
|
||||
recent tool outputs).
|
||||
- **Default:** `50000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.toolOutputMasking.minPrunableTokensThreshold`** (number):
|
||||
- **Description:** Minimum prunable tokens required to trigger a masking pass.
|
||||
- **Default:** `30000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.toolOutputMasking.protectLatestTurn`** (boolean):
|
||||
- **Description:** Ensures the absolute latest turn is never masked,
|
||||
regardless of token count.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.enableAgents`** (boolean):
|
||||
@@ -1630,7 +1629,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **`experimental.jitContext`** (boolean):
|
||||
- **Description:** Enable Just-In-Time (JIT) context loading.
|
||||
- **Default:** `false`
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.useOSC52Paste`** (boolean):
|
||||
@@ -1645,6 +1644,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
configured to allow it).
|
||||
- **Default:** `false`
|
||||
|
||||
- **`experimental.plan`** (boolean):
|
||||
- **Description:** Enable Plan Mode.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.taskTracker`** (boolean):
|
||||
- **Description:** Enable task tracker tools.
|
||||
- **Default:** `false`
|
||||
@@ -1690,8 +1694,25 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.contextManagement`** (boolean):
|
||||
- **Description:** Enable logic for context management.
|
||||
- **`experimental.agentHistoryTruncation`** (boolean):
|
||||
- **Description:** Enable truncation window logic for the Agent History
|
||||
Provider.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.agentHistoryTruncationThreshold`** (number):
|
||||
- **Description:** The maximum number of messages before history is truncated.
|
||||
- **Default:** `30`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.agentHistoryRetainedMessages`** (number):
|
||||
- **Description:** The number of recent messages to retain after truncation.
|
||||
- **Default:** `15`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.agentHistorySummarization`** (boolean):
|
||||
- **Description:** Enable summarization of truncated content via a small model
|
||||
for the Agent History Provider.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
@@ -1786,69 +1807,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
prioritize available tools dynamically.
|
||||
- **Default:** `[]`
|
||||
|
||||
#### `contextManagement`
|
||||
|
||||
- **`contextManagement.historyWindow.maxTokens`** (number):
|
||||
- **Description:** The number of tokens to allow before triggering
|
||||
compression.
|
||||
- **Default:** `150000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`contextManagement.historyWindow.retainedTokens`** (number):
|
||||
- **Description:** The number of tokens to always retain.
|
||||
- **Default:** `40000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`contextManagement.messageLimits.normalMaxTokens`** (number):
|
||||
- **Description:** The target number of tokens to budget for a normal
|
||||
conversation turn.
|
||||
- **Default:** `2500`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`contextManagement.messageLimits.retainedMaxTokens`** (number):
|
||||
- **Description:** The maximum number of tokens a single conversation turn can
|
||||
consume before truncation.
|
||||
- **Default:** `12000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`contextManagement.messageLimits.normalizationHeadRatio`** (number):
|
||||
- **Description:** The ratio of tokens to retain from the beginning of a
|
||||
truncated message (0.0 to 1.0).
|
||||
- **Default:** `0.25`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`contextManagement.tools.distillation.maxOutputTokens`** (number):
|
||||
- **Description:** Maximum tokens to show to the model when truncating large
|
||||
tool outputs.
|
||||
- **Default:** `10000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`contextManagement.tools.distillation.summarizationThresholdTokens`**
|
||||
(number):
|
||||
- **Description:** Threshold above which truncated tool outputs will be
|
||||
summarized by an LLM.
|
||||
- **Default:** `20000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`contextManagement.tools.outputMasking.protectionThresholdTokens`**
|
||||
(number):
|
||||
- **Description:** Minimum number of tokens to protect from masking (most
|
||||
recent tool outputs).
|
||||
- **Default:** `50000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`contextManagement.tools.outputMasking.minPrunableThresholdTokens`**
|
||||
(number):
|
||||
- **Description:** Minimum prunable tokens required to trigger a masking pass.
|
||||
- **Default:** `30000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`contextManagement.tools.outputMasking.protectLatestTurn`** (boolean):
|
||||
- **Description:** Ensures the absolute latest turn is never masked,
|
||||
regardless of token count.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `admin`
|
||||
|
||||
- **`admin.secureModeEnabled`** (boolean):
|
||||
|
||||
@@ -127,13 +127,6 @@ available combinations.
|
||||
| `background.unfocusList` | Move focus from background shell list to Gemini. | `Tab` |
|
||||
| `background.unfocusWarning` | Show warning when trying to move focus away from background shell. | `Tab` |
|
||||
|
||||
#### Extension Controls
|
||||
|
||||
| Command | Action | Keys |
|
||||
| ------------------ | ------------------------------------------- | ---- |
|
||||
| `extension.update` | Update the current extension if available. | `I` |
|
||||
| `extension.link` | Link the current extension to a local path. | `L` |
|
||||
|
||||
<!-- KEYBINDINGS-AUTOGEN:END -->
|
||||
|
||||
## Customizing Keybindings
|
||||
|
||||
@@ -29,12 +29,13 @@ To create your first policy:
|
||||
```toml
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "rm -rf"
|
||||
decision = "deny"
|
||||
commandPrefix = "git status"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
```
|
||||
3. **Run a command** that triggers the policy (e.g., ask Gemini CLI to
|
||||
`rm -rf /`). The tool will now be blocked automatically.
|
||||
`git status`). The tool will now execute automatically without prompting for
|
||||
confirmation.
|
||||
|
||||
## Core concepts
|
||||
|
||||
@@ -142,26 +143,25 @@ engine transforms this into a final priority using the following formula:
|
||||
|
||||
This system guarantees that:
|
||||
|
||||
- Admin policies always override User, Workspace, and Default policies (defined
|
||||
in policy TOML files).
|
||||
- Admin policies always override User, Workspace, and Default policies.
|
||||
- User policies override Workspace and Default policies.
|
||||
- Workspace policies override Default policies.
|
||||
- You can still order rules within a single tier with fine-grained control.
|
||||
|
||||
For example:
|
||||
|
||||
- A `priority: 50` rule in a Default policy TOML becomes `1.050`.
|
||||
- A `priority: 10` rule in a Workspace policy TOML becomes `2.010`.
|
||||
- A `priority: 100` rule in a User policy TOML becomes `3.100`.
|
||||
- A `priority: 20` rule in an Admin policy TOML becomes `4.020`.
|
||||
- A `priority: 50` rule in a Default policy file becomes `1.050`.
|
||||
- A `priority: 10` rule in a Workspace policy policy file becomes `2.010`.
|
||||
- A `priority: 100` rule in a User policy file becomes `3.100`.
|
||||
- A `priority: 20` rule in an Admin policy file becomes `4.020`.
|
||||
|
||||
### Approval modes
|
||||
|
||||
Approval modes allow the policy engine to apply different sets of rules based on
|
||||
the CLI's operational mode. A rule in a TOML policy file can be associated with
|
||||
one or more modes (e.g., `yolo`, `autoEdit`, `plan`). The rule will only be
|
||||
active if the CLI is running in one of its specified modes. If a rule has no
|
||||
modes specified, it is always active.
|
||||
the CLI's operational mode. A rule can be associated with one or more modes
|
||||
(e.g., `yolo`, `autoEdit`, `plan`). The rule will only be active if the CLI is
|
||||
running in one of its specified modes. If a rule has no modes specified, it is
|
||||
always active.
|
||||
|
||||
- `default`: The standard interactive mode where most write tools require
|
||||
confirmation.
|
||||
@@ -179,8 +179,8 @@ outcome.
|
||||
|
||||
A rule matches a tool call if all of its conditions are met:
|
||||
|
||||
1. **Tool name**: The `toolName` in the TOML rule must match the name of the
|
||||
tool being called.
|
||||
1. **Tool name**: The `toolName` in the rule must match the name of the tool
|
||||
being called.
|
||||
- **Wildcards**: You can use wildcards like `*`, `mcp_server_*`, or
|
||||
`mcp_*_toolName` to match multiple tools. See [Tool Name](#tool-name) for
|
||||
details.
|
||||
@@ -264,7 +264,7 @@ toolName = "run_shell_command"
|
||||
|
||||
# (Optional) The name of a subagent. If provided, the rule only applies to tool
|
||||
# calls made by this specific subagent.
|
||||
subagent = "codebase_investigator"
|
||||
subagent = "generalist"
|
||||
|
||||
# (Optional) The name of an MCP server. Can be combined with toolName
|
||||
# to form a composite FQN internally like "mcp_mcpName_toolName".
|
||||
@@ -419,6 +419,20 @@ decision = "ask_user"
|
||||
priority = 10
|
||||
```
|
||||
|
||||
**4. Targeting a tool name across all servers**
|
||||
|
||||
Use `mcpName = "*"` with a specific `toolName` to target that operation
|
||||
regardless of which server provides it.
|
||||
|
||||
```toml
|
||||
# Allow the `search` tool across all connected MCP servers
|
||||
[[rule]]
|
||||
mcpName = "*"
|
||||
toolName = "search"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
```
|
||||
|
||||
## Default policies
|
||||
|
||||
The Gemini CLI ships with a set of default policies to provide a safe
|
||||
|
||||
@@ -115,10 +115,10 @@ each tool.
|
||||
|
||||
### Web
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :-------------------------------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`google_web_search`](../tools/web-search.md) | `Search` | Performs a Google Search to find up-to-date information. |
|
||||
| [`web_fetch`](../tools/web-fetch.md) | `Fetch` | Retrieves and processes content from specific URLs. **Warning:** This tool can access local and private network addresses (e.g., localhost), which may pose a security risk if used with untrusted prompts. In Plan Mode, this tool requires explicit user confirmation. |
|
||||
| Tool | Kind | Description |
|
||||
| :-------------------------------------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`google_web_search`](../tools/web-search.md) | `Search` | Performs a Google Search to find up-to-date information. |
|
||||
| [`web_fetch`](../tools/web-fetch.md) | `Fetch` | Retrieves and processes content from specific URLs. **Warning:** This tool can access local and private network addresses (e.g., localhost), which may pose a security risk if used with untrusted prompts. |
|
||||
|
||||
## Under the hood
|
||||
|
||||
|
||||
@@ -138,10 +138,12 @@
|
||||
{ "label": "Plan mode", "slug": "docs/cli/plan-mode" },
|
||||
{
|
||||
"label": "Subagents",
|
||||
"badge": "🔬",
|
||||
"slug": "docs/core/subagents"
|
||||
},
|
||||
{
|
||||
"label": "Remote subagents",
|
||||
"badge": "🔬",
|
||||
"slug": "docs/core/remote-agents"
|
||||
},
|
||||
{ "label": "Rewind", "slug": "docs/cli/rewind" },
|
||||
|
||||
@@ -32,9 +32,7 @@ and planning.
|
||||
## 2. `exit_plan_mode` (ExitPlanMode)
|
||||
|
||||
`exit_plan_mode` signals that the planning phase is complete. It presents the
|
||||
finalized plan to the user and requests formal approval to start the
|
||||
implementation. The agent MUST reach an informal agreement with the user in the
|
||||
chat regarding the proposed strategy BEFORE calling this tool.
|
||||
finalized plan to the user and requests approval to start the implementation.
|
||||
|
||||
- **Tool name:** `exit_plan_mode`
|
||||
- **Display name:** Exit Plan Mode
|
||||
@@ -46,7 +44,7 @@ chat regarding the proposed strategy BEFORE calling this tool.
|
||||
- **Behavior:**
|
||||
- Validates that the `plan_path` is within the allowed directory and that the
|
||||
file exists and has content.
|
||||
- Presents the plan to the user for formal review.
|
||||
- Presents the plan to the user for review.
|
||||
- If the user approves the plan:
|
||||
- Switches the CLI's approval mode to the user's chosen approval mode (
|
||||
`DEFAULT` or `AUTO_EDIT`).
|
||||
@@ -58,5 +56,5 @@ chat regarding the proposed strategy BEFORE calling this tool.
|
||||
- On approval: A message indicating the plan was approved and the new approval
|
||||
mode.
|
||||
- On rejection: A message containing the user's feedback.
|
||||
- **Confirmation:** Yes. Shows the finalized plan and asks for user formal
|
||||
approval to proceed with implementation.
|
||||
- **Confirmation:** Yes. Shows the finalized plan and asks for user approval to
|
||||
proceed with implementation.
|
||||
|
||||
@@ -17,9 +17,6 @@ specific operations like summarization or extraction.
|
||||
## Technical behavior
|
||||
|
||||
- **Confirmation:** Triggers a confirmation dialog showing the converted URLs.
|
||||
- **Plan Mode:** In [Plan Mode](../cli/plan-mode.md), `web_fetch` is available
|
||||
but always requires explicit user confirmation (`ask_user`) due to security
|
||||
implications of accessing external or private network addresses.
|
||||
- **Processing:** Uses the Gemini API's `urlContext` for retrieval.
|
||||
- **Fallback:** If API access fails, the tool attempts to fetch raw content
|
||||
directly from your local machine.
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import { wasmLoader } from 'esbuild-plugin-wasm';
|
||||
let esbuild;
|
||||
try {
|
||||
esbuild = (await import('esbuild')).default;
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
console.error('esbuild not available - cannot build bundle');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
+4
-9
@@ -41,11 +41,6 @@ const commonRestrictedSyntaxRules = [
|
||||
message:
|
||||
'Do not use typeof to check object properties. Define a TypeScript interface and a type guard function instead.',
|
||||
},
|
||||
{
|
||||
selector: 'CatchClause > Identifier[name=/^_/]',
|
||||
message:
|
||||
'Do not use underscored identifiers in catch blocks. If the error is unused, use "catch {}". If it is used, remove the underscore.',
|
||||
},
|
||||
];
|
||||
|
||||
export default tseslint.config(
|
||||
@@ -134,7 +129,7 @@ export default tseslint.config(
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrors: 'all',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
// Prevent async errors from bypassing catch handlers
|
||||
@@ -341,7 +336,7 @@ export default tseslint.config(
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrors: 'all',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -365,7 +360,7 @@ export default tseslint.config(
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrors: 'all',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -427,7 +422,7 @@ export default tseslint.config(
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrors: 'all',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -212,56 +212,6 @@ The nightly workflow executes the full evaluation suite multiple times
|
||||
(currently 3 attempts) to account for non-determinism. These results are
|
||||
aggregated into a **Nightly Summary** attached to the workflow run.
|
||||
|
||||
## Regression Check Scripts
|
||||
|
||||
The project includes several scripts to automate high-signal regression checking
|
||||
in Pull Requests. These can also be run locally for debugging.
|
||||
|
||||
- **`scripts/get_trustworthy_evals.js`**: Analyzes nightly history to identify
|
||||
stable tests (80%+ aggregate pass rate).
|
||||
- **`scripts/run_regression_check.js`**: Runs a specific set of tests using the
|
||||
"Best-of-4" logic and "Dynamic Baseline Verification".
|
||||
- **`scripts/run_eval_regression.js`**: The main orchestrator that loops through
|
||||
models and generates the final PR report.
|
||||
|
||||
### Running Regression Checks Locally
|
||||
|
||||
You can simulate the PR regression check locally to verify your changes before
|
||||
pushing:
|
||||
|
||||
```bash
|
||||
# Run the full regression loop for a specific model
|
||||
MODEL_LIST=gemini-3-flash-preview node scripts/run_eval_regression.js
|
||||
```
|
||||
|
||||
To debug a specific failing test with the same logic used in CI:
|
||||
|
||||
```bash
|
||||
# 1. Get the Vitest pattern for trustworthy tests
|
||||
OUTPUT=$(node scripts/get_trustworthy_evals.js "gemini-3-flash-preview")
|
||||
|
||||
# 2. Run the regression logic for those tests
|
||||
node scripts/run_regression_check.js "gemini-3-flash-preview" "$OUTPUT"
|
||||
```
|
||||
|
||||
### The Regression Quality Bar
|
||||
|
||||
Because LLMs are non-deterministic, the PR regression check uses a high-signal
|
||||
probabilistic approach rather than a 100% pass requirement:
|
||||
|
||||
1. **Trustworthiness (60/80 Filter):** Only tests with a proven track record
|
||||
are run. A test must score at least **60% (2/3)** every single night and
|
||||
maintain an **80% aggregate** pass rate over the last 6 days.
|
||||
2. **The 50% Pass Rule:** In a PR, a test is considered a **Pass** if the model
|
||||
correctly performs the behavior at least half the time (**2 successes** out
|
||||
of up to 4 attempts).
|
||||
3. **Dynamic Baseline Verification:** If a test fails in a PR (e.g., 0/3), the
|
||||
system automatically checks the `main` branch. If it fails there too, it is
|
||||
marked as **Pre-existing** and cleared for the PR, ensuring you are only
|
||||
blocked by regressions caused by your specific changes.
|
||||
|
||||
## Fixing Evaluations
|
||||
|
||||
#### How to interpret the report:
|
||||
|
||||
- **Pass Rate (%)**: Each cell represents the percentage of successful runs for
|
||||
|
||||
+3
-82
@@ -15,9 +15,7 @@ import {
|
||||
describe('plan_mode', () => {
|
||||
const TEST_PREFIX = 'Plan Mode: ';
|
||||
const settings = {
|
||||
general: {
|
||||
plan: { enabled: true },
|
||||
},
|
||||
experimental: { plan: true },
|
||||
};
|
||||
|
||||
const getWriteTargets = (logs: any[]) =>
|
||||
@@ -174,8 +172,7 @@ describe('plan_mode', () => {
|
||||
params: {
|
||||
settings,
|
||||
},
|
||||
prompt:
|
||||
'I agree with the strategy to use a JWT-based login. Create a plan for a new login feature.',
|
||||
prompt: 'Create a plan for a new login feature.',
|
||||
assert: async (rig, result) => {
|
||||
await rig.waitForTelemetryReady();
|
||||
const toolLogs = rig.readToolLogs();
|
||||
@@ -212,7 +209,7 @@ describe('plan_mode', () => {
|
||||
'import { sum } from "./mathUtils";\nconsole.log(sum(1, 2));',
|
||||
},
|
||||
prompt:
|
||||
'I want to refactor our math utilities. I agree with the strategy to move the `sum` function from `src/mathUtils.ts` to a new file `src/basicMath.ts` and update `src/main.ts`. Please create a detailed implementation plan first, then execute it.',
|
||||
'I want to refactor our math utilities. Move the `sum` function from `src/mathUtils.ts` to a new file `src/basicMath.ts` and update `src/main.ts` to use the new file. Please create a detailed implementation plan first, then execute it.',
|
||||
assert: async (rig, result) => {
|
||||
const enterPlanCalled = await rig.waitForToolCall('enter_plan_mode');
|
||||
expect(
|
||||
@@ -284,80 +281,4 @@ describe('plan_mode', () => {
|
||||
assertModelHasOutput(result);
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
name: 'should transition from plan mode to normal execution and create a plan file from scratch',
|
||||
params: {
|
||||
settings,
|
||||
},
|
||||
prompt:
|
||||
'Enter plan mode and plan to create a new module called foo. The plan should be saved as foo-plan.md. Then, exit plan mode.',
|
||||
assert: async (rig, result) => {
|
||||
const enterPlanCalled = await rig.waitForToolCall('enter_plan_mode');
|
||||
expect(
|
||||
enterPlanCalled,
|
||||
'Expected enter_plan_mode tool to be called',
|
||||
).toBe(true);
|
||||
|
||||
const exitPlanCalled = await rig.waitForToolCall('exit_plan_mode');
|
||||
expect(exitPlanCalled, 'Expected exit_plan_mode tool to be called').toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
await rig.waitForTelemetryReady();
|
||||
const toolLogs = rig.readToolLogs();
|
||||
|
||||
// Check if the plan file was written successfully
|
||||
const planWrite = toolLogs.find(
|
||||
(log) =>
|
||||
log.toolRequest.name === 'write_file' &&
|
||||
log.toolRequest.args.includes('foo-plan.md'),
|
||||
);
|
||||
|
||||
expect(
|
||||
planWrite,
|
||||
'Expected write_file to be called for foo-plan.md',
|
||||
).toBeDefined();
|
||||
|
||||
expect(
|
||||
planWrite?.toolRequest.success,
|
||||
`Expected write_file to succeed, but got error: ${planWrite?.toolRequest.error}`,
|
||||
).toBe(true);
|
||||
|
||||
assertModelHasOutput(result);
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should not exit plan mode or draft before informal agreement',
|
||||
approvalMode: ApprovalMode.PLAN,
|
||||
params: {
|
||||
settings,
|
||||
},
|
||||
prompt: 'I need to build a new login feature. Please plan it.',
|
||||
assert: async (rig, result) => {
|
||||
await rig.waitForTelemetryReady();
|
||||
const toolLogs = rig.readToolLogs();
|
||||
|
||||
const exitPlanCall = toolLogs.find(
|
||||
(log) => log.toolRequest.name === 'exit_plan_mode',
|
||||
);
|
||||
expect(
|
||||
exitPlanCall,
|
||||
'Should NOT call exit_plan_mode before informal agreement',
|
||||
).toBeUndefined();
|
||||
|
||||
const planWrite = toolLogs.find(
|
||||
(log) =>
|
||||
log.toolRequest.name === 'write_file' &&
|
||||
log.toolRequest.args.includes('/plans/'),
|
||||
);
|
||||
expect(
|
||||
planWrite,
|
||||
'Should NOT draft the plan file before informal agreement',
|
||||
).toBeUndefined();
|
||||
|
||||
assertModelHasOutput(result);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
+19
-117
@@ -13,21 +13,8 @@ import { evalTest, TEST_AGENTS } from './test-helper.js';
|
||||
|
||||
const INDEX_TS = 'export const add = (a: number, b: number) => a + b;\n';
|
||||
|
||||
// A minimal package.json is used to provide a realistic workspace anchor.
|
||||
// This prevents the agent from making incorrect assumptions about the environment
|
||||
// and helps it properly navigate or act as if it is in a standard Node.js project.
|
||||
const MOCK_PACKAGE_JSON = JSON.stringify(
|
||||
{
|
||||
name: 'subagent-eval-project',
|
||||
version: '1.0.0',
|
||||
type: 'module',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
function readProjectFile(
|
||||
rig: { testDir: string | null },
|
||||
rig: { testDir?: string },
|
||||
relativePath: string,
|
||||
): string {
|
||||
return fs.readFileSync(path.join(rig.testDir!, relativePath), 'utf8');
|
||||
@@ -130,7 +117,15 @@ describe('subagent eval test cases', () => {
|
||||
files: {
|
||||
...TEST_AGENTS.TESTING_AGENT.asFile(),
|
||||
'index.ts': INDEX_TS,
|
||||
'package.json': MOCK_PACKAGE_JSON,
|
||||
'package.json': JSON.stringify(
|
||||
{
|
||||
name: 'subagent-eval-project',
|
||||
version: '1.0.0',
|
||||
type: 'module',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
assert: async (rig, _result) => {
|
||||
const toolLogs = rig.readToolLogs() as Array<{
|
||||
@@ -169,7 +164,15 @@ describe('subagent eval test cases', () => {
|
||||
...TEST_AGENTS.TESTING_AGENT.asFile(),
|
||||
'index.ts': INDEX_TS,
|
||||
'README.md': 'TODO: update the README.\n',
|
||||
'package.json': MOCK_PACKAGE_JSON,
|
||||
'package.json': JSON.stringify(
|
||||
{
|
||||
name: 'subagent-eval-project',
|
||||
version: '1.0.0',
|
||||
type: 'module',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
assert: async (rig, _result) => {
|
||||
const toolLogs = rig.readToolLogs() as Array<{
|
||||
@@ -187,105 +190,4 @@ describe('subagent eval test cases', () => {
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that the main agent can correctly select the appropriate subagent
|
||||
* from a large pool of available subagents (10 total).
|
||||
*/
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should select the correct subagent from a pool of 10 different agents',
|
||||
prompt: 'Please add a new SQL table migration for a user profile.',
|
||||
files: {
|
||||
...TEST_AGENTS.DOCS_AGENT.asFile(),
|
||||
...TEST_AGENTS.TESTING_AGENT.asFile(),
|
||||
...TEST_AGENTS.DATABASE_AGENT.asFile(),
|
||||
...TEST_AGENTS.CSS_AGENT.asFile(),
|
||||
...TEST_AGENTS.I18N_AGENT.asFile(),
|
||||
...TEST_AGENTS.SECURITY_AGENT.asFile(),
|
||||
...TEST_AGENTS.DEVOPS_AGENT.asFile(),
|
||||
...TEST_AGENTS.ANALYTICS_AGENT.asFile(),
|
||||
...TEST_AGENTS.ACCESSIBILITY_AGENT.asFile(),
|
||||
...TEST_AGENTS.MOBILE_AGENT.asFile(),
|
||||
'package.json': MOCK_PACKAGE_JSON,
|
||||
},
|
||||
assert: async (rig, _result) => {
|
||||
const toolLogs = rig.readToolLogs() as Array<{
|
||||
toolRequest: { name: string };
|
||||
}>;
|
||||
await rig.expectToolCallSuccess(['database-agent']);
|
||||
|
||||
// Ensure the generalist and other irrelevant specialists were not invoked
|
||||
const uncalledAgents = [
|
||||
'generalist',
|
||||
TEST_AGENTS.DOCS_AGENT.name,
|
||||
TEST_AGENTS.TESTING_AGENT.name,
|
||||
TEST_AGENTS.CSS_AGENT.name,
|
||||
TEST_AGENTS.I18N_AGENT.name,
|
||||
TEST_AGENTS.SECURITY_AGENT.name,
|
||||
TEST_AGENTS.DEVOPS_AGENT.name,
|
||||
TEST_AGENTS.ANALYTICS_AGENT.name,
|
||||
TEST_AGENTS.ACCESSIBILITY_AGENT.name,
|
||||
TEST_AGENTS.MOBILE_AGENT.name,
|
||||
];
|
||||
|
||||
for (const agentName of uncalledAgents) {
|
||||
expect(toolLogs.some((l) => l.toolRequest.name === agentName)).toBe(
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that the main agent can correctly select the appropriate subagent
|
||||
* from a large pool of available subagents, even when many irrelevant MCP tools are present.
|
||||
*
|
||||
* This test includes stress tests the subagent delegation with ~80 tools.
|
||||
*/
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should select the correct subagent from a pool of 10 different agents with MCP tools present',
|
||||
prompt: 'Please add a new SQL table migration for a user profile.',
|
||||
setup: async (rig) => {
|
||||
rig.addTestMcpServer('workspace-server', 'google-workspace');
|
||||
},
|
||||
files: {
|
||||
...TEST_AGENTS.DOCS_AGENT.asFile(),
|
||||
...TEST_AGENTS.TESTING_AGENT.asFile(),
|
||||
...TEST_AGENTS.DATABASE_AGENT.asFile(),
|
||||
...TEST_AGENTS.CSS_AGENT.asFile(),
|
||||
...TEST_AGENTS.I18N_AGENT.asFile(),
|
||||
...TEST_AGENTS.SECURITY_AGENT.asFile(),
|
||||
...TEST_AGENTS.DEVOPS_AGENT.asFile(),
|
||||
...TEST_AGENTS.ANALYTICS_AGENT.asFile(),
|
||||
...TEST_AGENTS.ACCESSIBILITY_AGENT.asFile(),
|
||||
...TEST_AGENTS.MOBILE_AGENT.asFile(),
|
||||
'package.json': MOCK_PACKAGE_JSON,
|
||||
},
|
||||
assert: async (rig, _result) => {
|
||||
const toolLogs = rig.readToolLogs() as Array<{
|
||||
toolRequest: { name: string };
|
||||
}>;
|
||||
await rig.expectToolCallSuccess(['database-agent']);
|
||||
|
||||
// Ensure the generalist and other irrelevant specialists were not invoked
|
||||
const uncalledAgents = [
|
||||
'generalist',
|
||||
TEST_AGENTS.DOCS_AGENT.name,
|
||||
TEST_AGENTS.TESTING_AGENT.name,
|
||||
TEST_AGENTS.CSS_AGENT.name,
|
||||
TEST_AGENTS.I18N_AGENT.name,
|
||||
TEST_AGENTS.SECURITY_AGENT.name,
|
||||
TEST_AGENTS.DEVOPS_AGENT.name,
|
||||
TEST_AGENTS.ANALYTICS_AGENT.name,
|
||||
TEST_AGENTS.ACCESSIBILITY_AGENT.name,
|
||||
TEST_AGENTS.MOBILE_AGENT.name,
|
||||
];
|
||||
|
||||
for (const agentName of uncalledAgents) {
|
||||
expect(toolLogs.some((l) => l.toolRequest.name === agentName)).toBe(
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,10 +61,6 @@ export async function internalEvalTest(evalCase: EvalCase) {
|
||||
try {
|
||||
rig.setup(evalCase.name, evalCase.params);
|
||||
|
||||
if (evalCase.setup) {
|
||||
await evalCase.setup(rig);
|
||||
}
|
||||
|
||||
if (evalCase.files) {
|
||||
await setupTestFiles(rig, evalCase.files);
|
||||
}
|
||||
@@ -375,7 +371,6 @@ export interface EvalCase {
|
||||
prompt: string;
|
||||
timeout?: number;
|
||||
files?: Record<string, string>;
|
||||
setup?: (rig: TestRig) => Promise<void> | void;
|
||||
/** Conversation history to pre-load via --resume. Each entry is a message object with type, content, etc. */
|
||||
messages?: Record<string, unknown>[];
|
||||
/** Session ID for the resumed session. Auto-generated if not provided. */
|
||||
|
||||
@@ -113,21 +113,4 @@ describe('tracker_mode', () => {
|
||||
assertModelHasOutput(result);
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should correctly identify the task tracker storage location from the system prompt',
|
||||
params: {
|
||||
settings: { experimental: { taskTracker: true } },
|
||||
},
|
||||
prompt:
|
||||
'Where is my task tracker storage located? Please provide the absolute path in your response.',
|
||||
assert: async (rig, result) => {
|
||||
// The rig sets GEMINI_CLI_HOME to rig.homeDir
|
||||
const homeDir = rig.homeDir!;
|
||||
// The response should contain the dynamic path which includes the home directory
|
||||
// and follows the .gemini/tmp/.../tracker structure.
|
||||
expect(result).toContain(homeDir);
|
||||
expect(result).toMatch(/\.gemini\/tmp\/.*\/tracker/);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
describe('update_topic_behavior', () => {
|
||||
// Constants for tool names and params for robustness
|
||||
const UPDATE_TOPIC_TOOL_NAME = 'update_topic';
|
||||
|
||||
/**
|
||||
* Verifies the desired behavior of the update_topic tool. update_topic is used by the
|
||||
* agent to share periodic, concise updates about what the agent is working on, independent
|
||||
* of the regular model output and/or thoughts. This tool is expected to be called at least
|
||||
* at the start and end of the session, and typically at least once in the middle, but no
|
||||
* more than 1/4 turns.
|
||||
*/
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'update_topic should be used at start, end and middle for complex tasks',
|
||||
prompt: `Create a simple users REST API using Express.
|
||||
1. Initialize a new npm project and install express.
|
||||
2. Create src/app.ts as the main entry point.
|
||||
3. Create src/routes/userRoutes.ts for user routes.
|
||||
4. Create src/controllers/userController.ts for user logic.
|
||||
5. Implement GET /users, POST /users, and GET /users/:id using an in-memory array.
|
||||
6. Add a 'start' script to package.json.
|
||||
7. Finally, run a quick grep to verify the routes are in src/app.ts.`,
|
||||
files: {
|
||||
'package.json': JSON.stringify(
|
||||
{
|
||||
name: 'users-api',
|
||||
version: '1.0.0',
|
||||
private: true,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
'.gemini/settings.json': JSON.stringify({
|
||||
experimental: {
|
||||
topicUpdateNarration: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
assert: async (rig, result) => {
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const topicCalls = toolLogs.filter(
|
||||
(l) => l.toolRequest.name === UPDATE_TOPIC_TOOL_NAME,
|
||||
);
|
||||
|
||||
// 1. Assert that update_topic is called at least 3 times (start, middle, end)
|
||||
expect(
|
||||
topicCalls.length,
|
||||
`Expected at least 3 update_topic calls, but found ${topicCalls.length}`,
|
||||
).toBeGreaterThanOrEqual(3);
|
||||
|
||||
// 2. Assert update_topic is called at the very beginning (first tool call)
|
||||
expect(
|
||||
toolLogs[0].toolRequest.name,
|
||||
'First tool call should be update_topic',
|
||||
).toBe(UPDATE_TOPIC_TOOL_NAME);
|
||||
|
||||
// 3. Assert update_topic is called near the end
|
||||
const lastTopicCallIndex = toolLogs
|
||||
.map((l) => l.toolRequest.name)
|
||||
.lastIndexOf(UPDATE_TOPIC_TOOL_NAME);
|
||||
expect(
|
||||
lastTopicCallIndex,
|
||||
'Expected update_topic to be used near the end of the task',
|
||||
).toBeGreaterThanOrEqual(toolLogs.length * 0.7);
|
||||
|
||||
// 4. Assert there is at least one update_topic call in the middle (between start and end phases)
|
||||
const middleTopicCalls = topicCalls.slice(1, -1);
|
||||
|
||||
expect(
|
||||
middleTopicCalls.length,
|
||||
'Expected at least one update_topic call in the middle of the task',
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// 5. Turn Ratio Assertion: update_topic should be <= 1/2 of total turns.
|
||||
// We only enforce this for tasks that take more than 5 turns, as shorter tasks
|
||||
// naturally have a higher ratio when following the "start, middle, end" rule.
|
||||
const uniquePromptIds = new Set(
|
||||
toolLogs
|
||||
.map((l) => l.toolRequest.prompt_id)
|
||||
.filter((id) => id !== undefined),
|
||||
);
|
||||
const totalTurns = uniquePromptIds.size;
|
||||
|
||||
if (totalTurns > 5) {
|
||||
const topicTurns = new Set(
|
||||
topicCalls
|
||||
.map((l) => l.toolRequest.prompt_id)
|
||||
.filter((id) => id !== undefined),
|
||||
);
|
||||
const topicTurnCount = topicTurns.size;
|
||||
|
||||
const ratio = topicTurnCount / totalTurns;
|
||||
|
||||
expect(
|
||||
ratio,
|
||||
`update_topic was used in ${topicTurnCount} out of ${totalTurns} turns (${(ratio * 100).toFixed(1)}%). Expected <= 50%.`,
|
||||
).toBeLessThanOrEqual(0.5);
|
||||
|
||||
// Ideal ratio is closer to 1/5 (20%). We log high usage as a warning.
|
||||
if (ratio > 0.25) {
|
||||
console.warn(
|
||||
`[Efficiency Warning] update_topic usage is high: ${(ratio * 100).toFixed(1)}% (Goal: ~20%)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Part 1. "}],"role":"model"},"index":0}]},{"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":10,"totalTokenCount":110}},{"candidates":[{"content":{"parts":[{"text":"Part 2."}],"role":"model"},"index":0,"finishReason":"STOP"}]}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Part 1. "}],"role":"model"},"index":0}]},{"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":10,"totalTokenCount":110}},{"candidates":[{"content":{"parts":[{"text":"Part 2."}],"role":"model"},"index":0}],"finishReason":"STOP"}]}
|
||||
|
||||
@@ -10,13 +10,8 @@ import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
|
||||
import { env } from 'node:process';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
// Browser agent Chrome DevTools MCP connection is flaky in Docker sandbox.
|
||||
// See: https://github.com/google-gemini/gemini-cli/issues/24382
|
||||
const isDockerSandbox = env['GEMINI_SANDBOX'] === 'docker';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
@@ -64,146 +59,122 @@ describe.skipIf(!chromeAvailable)('browser-policy', () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it.skipIf(isDockerSandbox)(
|
||||
'should skip confirmation when "Allow all server tools for this session" is chosen',
|
||||
async () => {
|
||||
rig.setup('browser-policy-skip-confirmation', {
|
||||
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
|
||||
settings: {
|
||||
agents: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated',
|
||||
allowedDomains: ['example.com'],
|
||||
it('should skip confirmation when "Allow all server tools for this session" is chosen', async () => {
|
||||
rig.setup('browser-policy-skip-confirmation', {
|
||||
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
|
||||
settings: {
|
||||
agents: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated',
|
||||
allowedDomains: ['example.com'],
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Manually trust the folder to avoid the dialog and enable option 3
|
||||
const geminiDir = join(rig.homeDir!, '.gemini');
|
||||
mkdirSync(geminiDir, { recursive: true });
|
||||
// Manually trust the folder to avoid the dialog and enable option 3
|
||||
const geminiDir = join(rig.homeDir!, '.gemini');
|
||||
mkdirSync(geminiDir, { recursive: true });
|
||||
|
||||
// Write to trustedFolders.json
|
||||
const trustedFoldersPath = join(geminiDir, 'trustedFolders.json');
|
||||
const trustedFolders = {
|
||||
[rig.testDir!]: 'TRUST_FOLDER',
|
||||
};
|
||||
writeFileSync(
|
||||
trustedFoldersPath,
|
||||
JSON.stringify(trustedFolders, null, 2),
|
||||
);
|
||||
// Write to trustedFolders.json
|
||||
const trustedFoldersPath = join(geminiDir, 'trustedFolders.json');
|
||||
const trustedFolders = {
|
||||
[rig.testDir!]: 'TRUST_FOLDER',
|
||||
};
|
||||
writeFileSync(trustedFoldersPath, JSON.stringify(trustedFolders, null, 2));
|
||||
|
||||
// Force confirmation for browser agent.
|
||||
// NOTE: We don't force confirm browser tools here because "Allow all server tools"
|
||||
// adds a rule with ALWAYS_ALLOW_PRIORITY (3.9x) which would be overshadowed by
|
||||
// a rule in the user tier (4.x) like the one from this TOML.
|
||||
// By removing the explicit mcp rule, the first MCP tool will still prompt
|
||||
// due to default approvalMode = 'default', and then "Allow all" will correctly
|
||||
// bypass subsequent tools.
|
||||
const policyFile = join(rig.testDir!, 'force-confirm.toml');
|
||||
writeFileSync(
|
||||
policyFile,
|
||||
`
|
||||
// Force confirmation for browser agent.
|
||||
// NOTE: We don't force confirm browser tools here because "Allow all server tools"
|
||||
// adds a rule with ALWAYS_ALLOW_PRIORITY (3.9x) which would be overshadowed by
|
||||
// a rule in the user tier (4.x) like the one from this TOML.
|
||||
// By removing the explicit mcp rule, the first MCP tool will still prompt
|
||||
// due to default approvalMode = 'default', and then "Allow all" will correctly
|
||||
// bypass subsequent tools.
|
||||
const policyFile = join(rig.testDir!, 'force-confirm.toml');
|
||||
writeFileSync(
|
||||
policyFile,
|
||||
`
|
||||
[[rule]]
|
||||
name = "Force confirm browser_agent"
|
||||
toolName = "browser_agent"
|
||||
decision = "ask_user"
|
||||
priority = 200
|
||||
`,
|
||||
);
|
||||
);
|
||||
|
||||
// Update settings.json in both project and home directories to point to the policy file
|
||||
for (const baseDir of [rig.testDir!, rig.homeDir!]) {
|
||||
const settingsPath = join(baseDir, '.gemini', 'settings.json');
|
||||
if (existsSync(settingsPath)) {
|
||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
settings.policyPaths = [policyFile];
|
||||
// Ensure folder trust is enabled
|
||||
settings.security = settings.security || {};
|
||||
settings.security.folderTrust = settings.security.folderTrust || {};
|
||||
settings.security.folderTrust.enabled = true;
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||
}
|
||||
// Update settings.json in both project and home directories to point to the policy file
|
||||
for (const baseDir of [rig.testDir!, rig.homeDir!]) {
|
||||
const settingsPath = join(baseDir, '.gemini', 'settings.json');
|
||||
if (existsSync(settingsPath)) {
|
||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
settings.policyPaths = [policyFile];
|
||||
// Ensure folder trust is enabled
|
||||
settings.security = settings.security || {};
|
||||
settings.security.folderTrust = settings.security.folderTrust || {};
|
||||
settings.security.folderTrust.enabled = true;
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
const run = await rig.runInteractive({
|
||||
approvalMode: 'default',
|
||||
env: {
|
||||
GEMINI_CLI_INTEGRATION_TEST: 'true',
|
||||
},
|
||||
});
|
||||
const run = await rig.runInteractive({
|
||||
approvalMode: 'default',
|
||||
env: {
|
||||
GEMINI_CLI_INTEGRATION_TEST: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
await run.sendKeys(
|
||||
'Open https://example.com and check if there is a heading\r',
|
||||
);
|
||||
await run.sendKeys('\r');
|
||||
await run.sendKeys(
|
||||
'Open https://example.com and check if there is a heading\r',
|
||||
);
|
||||
await run.sendKeys('\r');
|
||||
|
||||
// Handle confirmations.
|
||||
// 1. Initial browser_agent delegation (likely only 3 options, so use option 1: Allow once)
|
||||
await poll(
|
||||
() => stripAnsi(run.output).toLowerCase().includes('action required'),
|
||||
60000,
|
||||
1000,
|
||||
);
|
||||
await run.sendKeys('1\r');
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
// Handle confirmations.
|
||||
// 1. Initial browser_agent delegation (likely only 3 options, so use option 1: Allow once)
|
||||
await poll(
|
||||
() => stripAnsi(run.output).toLowerCase().includes('action required'),
|
||||
60000,
|
||||
1000,
|
||||
);
|
||||
await run.sendKeys('1\r');
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
// Handle privacy notice
|
||||
await poll(
|
||||
() => stripAnsi(run.output).toLowerCase().includes('privacy notice'),
|
||||
5000,
|
||||
100,
|
||||
);
|
||||
await run.sendKeys('1\r');
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
// Handle privacy notice
|
||||
await poll(
|
||||
() => stripAnsi(run.output).toLowerCase().includes('privacy notice'),
|
||||
5000,
|
||||
100,
|
||||
);
|
||||
await run.sendKeys('1\r');
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
|
||||
// new_page (MCP tool, should have 4 options, use option 3: Allow all server tools)
|
||||
await poll(
|
||||
() => {
|
||||
const stripped = stripAnsi(run.output).toLowerCase();
|
||||
return (
|
||||
stripped.includes('new_page') &&
|
||||
stripped.includes('allow all server tools for this session')
|
||||
);
|
||||
},
|
||||
60000,
|
||||
1000,
|
||||
);
|
||||
// new_page (MCP tool, should have 4 options, use option 3: Allow all server tools)
|
||||
await poll(
|
||||
() => {
|
||||
const stripped = stripAnsi(run.output).toLowerCase();
|
||||
return (
|
||||
stripped.includes('new_page') &&
|
||||
stripped.includes('allow all server tools for this session')
|
||||
);
|
||||
},
|
||||
60000,
|
||||
1000,
|
||||
);
|
||||
|
||||
// Select "Allow all server tools for this session" (option 3)
|
||||
await run.sendKeys('3\r');
|
||||
// Select "Allow all server tools for this session" (option 3)
|
||||
await run.sendKeys('3\r');
|
||||
await new Promise((r) => setTimeout(r, 30000));
|
||||
|
||||
// Wait for the browser agent to finish (success or failure)
|
||||
await poll(
|
||||
() => {
|
||||
const stripped = stripAnsi(run.output).toLowerCase();
|
||||
return (
|
||||
stripped.includes('completed successfully') ||
|
||||
stripped.includes('agent error')
|
||||
);
|
||||
},
|
||||
120000,
|
||||
1000,
|
||||
);
|
||||
const output = stripAnsi(run.output).toLowerCase();
|
||||
|
||||
const output = stripAnsi(run.output).toLowerCase();
|
||||
|
||||
expect(output).toContain('browser_agent');
|
||||
// The test validates that "Allow all server tools" skips subsequent
|
||||
// tool confirmations — the browser agent may still fail due to
|
||||
// Chrome/MCP issues in CI, which is acceptable for this policy test.
|
||||
expect(
|
||||
output.includes('completed successfully') ||
|
||||
output.includes('agent error'),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
expect(output).toContain('browser_agent');
|
||||
expect(output).toContain('completed successfully');
|
||||
});
|
||||
|
||||
it('should show the visible warning when browser agent starts in existing session mode', async () => {
|
||||
rig.setup('browser-session-warning', {
|
||||
|
||||
@@ -121,7 +121,6 @@ describe('file-system', () => {
|
||||
|
||||
const result = await rig.run({
|
||||
args: `write "hello" to "${fileName}" and then stop. Do not perform any other actions.`,
|
||||
timeout: 600000, // 10 min — real LLM can be slow in Docker sandbox
|
||||
});
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('write_file');
|
||||
|
||||
@@ -23,9 +23,7 @@ describe('Plan Mode', () => {
|
||||
'should allow read-only tools but deny write tools in plan mode',
|
||||
{
|
||||
settings: {
|
||||
general: {
|
||||
plan: { enabled: true },
|
||||
},
|
||||
experimental: { plan: true },
|
||||
tools: {
|
||||
core: [
|
||||
'run_shell_command',
|
||||
@@ -69,12 +67,15 @@ describe('Plan Mode', () => {
|
||||
|
||||
await rig.setup(testName, {
|
||||
settings: {
|
||||
experimental: { plan: true },
|
||||
tools: {
|
||||
core: ['write_file', 'read_file', 'list_directory'],
|
||||
},
|
||||
general: {
|
||||
plan: { enabled: true, directory: plansDir },
|
||||
defaultApprovalMode: 'plan',
|
||||
plan: {
|
||||
directory: plansDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -119,19 +120,22 @@ describe('Plan Mode', () => {
|
||||
|
||||
await rig.setup(testName, {
|
||||
settings: {
|
||||
experimental: { plan: true },
|
||||
tools: {
|
||||
core: ['write_file', 'read_file', 'list_directory'],
|
||||
},
|
||||
general: {
|
||||
plan: { enabled: true, directory: plansDir },
|
||||
defaultApprovalMode: 'plan',
|
||||
plan: {
|
||||
directory: plansDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await rig.run({
|
||||
approvalMode: 'plan',
|
||||
args: 'Attempt to create a file named "hello.txt" in the current directory. Do not create a plan file, try to write hello.txt directly.',
|
||||
args: 'Create a file called hello.txt in the current directory.',
|
||||
});
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
@@ -152,9 +156,7 @@ describe('Plan Mode', () => {
|
||||
it('should be able to enter plan mode from default mode', async () => {
|
||||
await rig.setup('should be able to enter plan mode from default mode', {
|
||||
settings: {
|
||||
general: {
|
||||
plan: { enabled: true },
|
||||
},
|
||||
experimental: { plan: true },
|
||||
tools: {
|
||||
core: ['enter_plan_mode'],
|
||||
allowed: ['enter_plan_mode'],
|
||||
@@ -182,12 +184,15 @@ describe('Plan Mode', () => {
|
||||
|
||||
await rig.setup(testName, {
|
||||
settings: {
|
||||
experimental: { plan: true },
|
||||
tools: {
|
||||
core: ['write_file', 'read_file', 'list_directory'],
|
||||
},
|
||||
general: {
|
||||
plan: { enabled: true, directory: plansDir },
|
||||
defaultApprovalMode: 'plan',
|
||||
plan: {
|
||||
directory: plansDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Generated
+50
-25
@@ -11,7 +11,7 @@
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink": "npm:@jrichman/ink@6.5.0",
|
||||
"latest-version": "^9.0.0",
|
||||
"node-fetch-native": "^1.6.7",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
@@ -92,6 +92,46 @@
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@alcalzone/ansi-tokenize": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.2.tgz",
|
||||
"integrity": "sha512-mkOh+Wwawzuf5wa30bvc4nA+Qb6DIrGWgBhRR/Pw4T9nsgYait8izvXkNyU78D6Wcu3Z+KUdwCmLCxlWjEotYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"is-fullwidth-code-point": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@alcalzone/ansi-tokenize/node_modules/is-fullwidth-code-point": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
@@ -10049,13 +10089,14 @@
|
||||
},
|
||||
"node_modules/ink": {
|
||||
"name": "@jrichman/ink",
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.3.tgz",
|
||||
"integrity": "sha512-0v4S7TbbF2tpQrfqH1btwLgTgH+K0vY2BJbokTE5Lk1KBr4TqZ+Pyo+geSD5F+zytX6G2ajGHBQyHk8yGK4C7A==",
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.5.0.tgz",
|
||||
"integrity": "sha512-S4g/ng7fPZmFwclO82iWkOce8vDLy/FIDgHIfkCWGOehqHe6dexHsmq3kNQD21okh198pA5SAQTCqNQJb/svRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
"ansi-styles": "^6.2.3",
|
||||
"ansi-styles": "^6.2.1",
|
||||
"auto-bind": "^5.0.1",
|
||||
"chalk": "^5.6.0",
|
||||
"cli-boxes": "^3.0.0",
|
||||
@@ -10064,7 +10105,6 @@
|
||||
"code-excerpt": "^4.0.0",
|
||||
"es-toolkit": "^1.39.10",
|
||||
"indent-string": "^5.0.0",
|
||||
"is-fullwidth-code-point": "^5.0.0",
|
||||
"is-in-ci": "^2.0.0",
|
||||
"mnemonist": "^0.40.3",
|
||||
"patch-console": "^2.0.0",
|
||||
@@ -10134,9 +10174,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ink/node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -10157,21 +10197,6 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ink/node_modules/is-fullwidth-code-point": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ink/node_modules/is-in-ci": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz",
|
||||
@@ -17526,7 +17551,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^12.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink": "npm:@jrichman/ink@6.5.0",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
|
||||
+3
-3
@@ -38,7 +38,7 @@
|
||||
"build:packages": "npm run build --workspaces",
|
||||
"build:sandbox": "node scripts/build_sandbox.js",
|
||||
"build:binary": "node scripts/build_binary.js",
|
||||
"bundle": "npm run generate && npm run build --workspace=@google/gemini-cli-devtools && npm run bundle:browser-mcp -w @google/gemini-cli-core && node esbuild.config.js && node scripts/copy_bundle_assets.js",
|
||||
"bundle": "npm run generate && npm run build --workspace=@google/gemini-cli-devtools && node esbuild.config.js && node scripts/copy_bundle_assets.js",
|
||||
"test": "npm run test --workspaces --if-present && npm run test:sea-launch",
|
||||
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts && npm run test:sea-launch",
|
||||
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
|
||||
@@ -68,7 +68,7 @@
|
||||
"pre-commit": "node scripts/pre-commit.js"
|
||||
},
|
||||
"overrides": {
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink": "npm:@jrichman/ink@6.5.0",
|
||||
"wrap-ansi": "9.0.2",
|
||||
"cliui": {
|
||||
"wrap-ansi": "7.0.0"
|
||||
@@ -136,7 +136,7 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink": "npm:@jrichman/ink@6.5.0",
|
||||
"latest-version": "^9.0.0",
|
||||
"node-fetch-native": "^1.6.7",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
|
||||
@@ -98,7 +98,7 @@ export class RestoreCommand implements Command {
|
||||
name: this.name,
|
||||
data: restoreResult,
|
||||
};
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
return {
|
||||
name: this.name,
|
||||
data: {
|
||||
@@ -142,7 +142,7 @@ export class ListCheckpointsCommand implements Command {
|
||||
content: JSON.stringify(checkpointInfoList),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
return {
|
||||
name: this.name,
|
||||
data: {
|
||||
|
||||
@@ -424,8 +424,27 @@ describe('loadConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('authentication logic', () => {
|
||||
const setupConfigMock = (refreshAuthMock: ReturnType<typeof vi.fn>) => {
|
||||
describe('authentication fallback', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('USE_CCPA', 'true');
|
||||
vi.stubEnv('GEMINI_API_KEY', '');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should fall back to COMPUTE_ADC in Cloud Shell if LOGIN_WITH_GOOGLE fails', async () => {
|
||||
vi.stubEnv('CLOUD_SHELL', 'true');
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(false);
|
||||
const refreshAuthMock = vi.fn().mockImplementation((authType) => {
|
||||
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
|
||||
throw new FatalAuthenticationError('Non-interactive session');
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
// Update the mock implementation for this test
|
||||
vi.mocked(Config).mockImplementation(
|
||||
(params: unknown) =>
|
||||
({
|
||||
@@ -438,88 +457,159 @@ describe('loadConfig', () => {
|
||||
setRemoteAdminSettings: vi.fn(),
|
||||
}) as unknown as Config,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('USE_CCPA', 'true');
|
||||
vi.stubEnv('GEMINI_API_KEY', '');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should attempt COMPUTE_ADC by default and bypass LOGIN_WITH_GOOGLE if successful', async () => {
|
||||
const refreshAuthMock = vi.fn().mockResolvedValue(undefined);
|
||||
setupConfigMock(refreshAuthMock);
|
||||
|
||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||
|
||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
|
||||
expect(refreshAuthMock).not.toHaveBeenCalledWith(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to LOGIN_WITH_GOOGLE if COMPUTE_ADC fails and interactive mode is available', async () => {
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(false);
|
||||
const refreshAuthMock = vi.fn().mockImplementation((authType) => {
|
||||
if (authType === AuthType.COMPUTE_ADC) {
|
||||
return Promise.reject(new Error('ADC failed'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
setupConfigMock(refreshAuthMock);
|
||||
|
||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||
|
||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
|
||||
expect(refreshAuthMock).toHaveBeenCalledWith(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
);
|
||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
|
||||
});
|
||||
|
||||
it('should throw FatalAuthenticationError in headless mode if COMPUTE_ADC fails', async () => {
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(true);
|
||||
|
||||
it('should not fall back to COMPUTE_ADC if not in cloud environment', async () => {
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(false);
|
||||
const refreshAuthMock = vi.fn().mockImplementation((authType) => {
|
||||
if (authType === AuthType.COMPUTE_ADC) {
|
||||
return Promise.reject(new Error('ADC not found'));
|
||||
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
|
||||
throw new FatalAuthenticationError('Non-interactive session');
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
setupConfigMock(refreshAuthMock);
|
||||
|
||||
vi.mocked(Config).mockImplementation(
|
||||
(params: unknown) =>
|
||||
({
|
||||
...(params as object),
|
||||
initialize: vi.fn(),
|
||||
waitForMcpInit: vi.fn(),
|
||||
refreshAuth: refreshAuthMock,
|
||||
getExperiments: vi.fn().mockReturnValue({ flags: {} }),
|
||||
getRemoteAdminSettings: vi.fn(),
|
||||
setRemoteAdminSettings: vi.fn(),
|
||||
}) as unknown as Config,
|
||||
);
|
||||
|
||||
await expect(
|
||||
loadConfig(mockSettings, mockExtensionLoader, taskId),
|
||||
).rejects.toThrow(
|
||||
'COMPUTE_ADC failed: ADC not found. (LOGIN_WITH_GOOGLE fallback skipped due to headless mode. Run in an interactive terminal to use OAuth.)',
|
||||
).rejects.toThrow('Non-interactive session');
|
||||
|
||||
expect(refreshAuthMock).toHaveBeenCalledWith(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
);
|
||||
expect(refreshAuthMock).not.toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
|
||||
});
|
||||
|
||||
it('should skip LOGIN_WITH_GOOGLE and use COMPUTE_ADC directly in headless Cloud Shell', async () => {
|
||||
vi.stubEnv('CLOUD_SHELL', 'true');
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(true);
|
||||
|
||||
const refreshAuthMock = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
vi.mocked(Config).mockImplementation(
|
||||
(params: unknown) =>
|
||||
({
|
||||
...(params as object),
|
||||
initialize: vi.fn(),
|
||||
waitForMcpInit: vi.fn(),
|
||||
refreshAuth: refreshAuthMock,
|
||||
getExperiments: vi.fn().mockReturnValue({ flags: {} }),
|
||||
getRemoteAdminSettings: vi.fn(),
|
||||
setRemoteAdminSettings: vi.fn(),
|
||||
}) as unknown as Config,
|
||||
);
|
||||
|
||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
|
||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||
|
||||
expect(refreshAuthMock).not.toHaveBeenCalledWith(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
);
|
||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
|
||||
});
|
||||
|
||||
it('should include both original and fallback error when LOGIN_WITH_GOOGLE fallback fails', async () => {
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(false);
|
||||
it('should skip LOGIN_WITH_GOOGLE and use COMPUTE_ADC directly if GEMINI_CLI_USE_COMPUTE_ADC is true', async () => {
|
||||
vi.stubEnv('GEMINI_CLI_USE_COMPUTE_ADC', 'true');
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(false); // Even if not headless
|
||||
|
||||
const refreshAuthMock = vi.fn().mockImplementation((authType) => {
|
||||
if (authType === AuthType.COMPUTE_ADC) {
|
||||
throw new Error('ADC failed');
|
||||
}
|
||||
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
|
||||
throw new FatalAuthenticationError('OAuth failed');
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
setupConfigMock(refreshAuthMock);
|
||||
const refreshAuthMock = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
vi.mocked(Config).mockImplementation(
|
||||
(params: unknown) =>
|
||||
({
|
||||
...(params as object),
|
||||
initialize: vi.fn(),
|
||||
waitForMcpInit: vi.fn(),
|
||||
refreshAuth: refreshAuthMock,
|
||||
getExperiments: vi.fn().mockReturnValue({ flags: {} }),
|
||||
getRemoteAdminSettings: vi.fn(),
|
||||
setRemoteAdminSettings: vi.fn(),
|
||||
}) as unknown as Config,
|
||||
);
|
||||
|
||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||
|
||||
expect(refreshAuthMock).not.toHaveBeenCalledWith(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
);
|
||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
|
||||
});
|
||||
|
||||
it('should throw FatalAuthenticationError in headless mode if no ADC fallback available', async () => {
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(true);
|
||||
|
||||
const refreshAuthMock = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
vi.mocked(Config).mockImplementation(
|
||||
(params: unknown) =>
|
||||
({
|
||||
...(params as object),
|
||||
initialize: vi.fn(),
|
||||
waitForMcpInit: vi.fn(),
|
||||
refreshAuth: refreshAuthMock,
|
||||
getExperiments: vi.fn().mockReturnValue({ flags: {} }),
|
||||
getRemoteAdminSettings: vi.fn(),
|
||||
setRemoteAdminSettings: vi.fn(),
|
||||
}) as unknown as Config,
|
||||
);
|
||||
|
||||
await expect(
|
||||
loadConfig(mockSettings, mockExtensionLoader, taskId),
|
||||
).rejects.toThrow(
|
||||
'OAuth failed. The initial COMPUTE_ADC attempt also failed: ADC failed',
|
||||
'Interactive terminal required for LOGIN_WITH_GOOGLE. Run in an interactive terminal or set GEMINI_CLI_USE_COMPUTE_ADC=true to use Application Default Credentials.',
|
||||
);
|
||||
|
||||
expect(refreshAuthMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should include both original and fallback error when COMPUTE_ADC fallback fails', async () => {
|
||||
vi.stubEnv('CLOUD_SHELL', 'true');
|
||||
vi.mocked(isHeadlessMode).mockReturnValue(false);
|
||||
|
||||
const refreshAuthMock = vi.fn().mockImplementation((authType) => {
|
||||
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
|
||||
throw new FatalAuthenticationError('OAuth failed');
|
||||
}
|
||||
if (authType === AuthType.COMPUTE_ADC) {
|
||||
throw new Error('ADC failed');
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
vi.mocked(Config).mockImplementation(
|
||||
(params: unknown) =>
|
||||
({
|
||||
...(params as object),
|
||||
initialize: vi.fn(),
|
||||
waitForMcpInit: vi.fn(),
|
||||
refreshAuth: refreshAuthMock,
|
||||
getExperiments: vi.fn().mockReturnValue({ flags: {} }),
|
||||
getRemoteAdminSettings: vi.fn(),
|
||||
setRemoteAdminSettings: vi.fn(),
|
||||
}) as unknown as Config,
|
||||
);
|
||||
|
||||
await expect(
|
||||
loadConfig(mockSettings, mockExtensionLoader, taskId),
|
||||
).rejects.toThrow(
|
||||
'OAuth failed. Fallback to COMPUTE_ADC also failed: ADC failed',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
ExperimentFlags,
|
||||
isHeadlessMode,
|
||||
FatalAuthenticationError,
|
||||
isCloudShell,
|
||||
PolicyDecision,
|
||||
PRIORITY_YOLO_ALLOW_ALL,
|
||||
type TelemetryTarget,
|
||||
@@ -42,6 +43,7 @@ export async function loadConfig(
|
||||
taskId: string,
|
||||
): Promise<Config> {
|
||||
const workspaceDir = process.cwd();
|
||||
const adcFilePath = process.env['GOOGLE_APPLICATION_CREDENTIALS'];
|
||||
|
||||
const folderTrust =
|
||||
settings.folderTrust === true ||
|
||||
@@ -190,7 +192,7 @@ export async function loadConfig(
|
||||
await config.waitForMcpInit();
|
||||
startupProfiler.flush(config);
|
||||
|
||||
await refreshAuthentication(config, 'Config');
|
||||
await refreshAuthentication(config, adcFilePath, 'Config');
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -261,51 +263,75 @@ function findEnvFile(startDir: string): string | null {
|
||||
|
||||
async function refreshAuthentication(
|
||||
config: Config,
|
||||
adcFilePath: string | undefined,
|
||||
logPrefix: string,
|
||||
): Promise<void> {
|
||||
if (process.env['USE_CCPA']) {
|
||||
logger.info(`[${logPrefix}] Using CCPA Auth:`);
|
||||
|
||||
logger.info(`[${logPrefix}] Attempting COMPUTE_ADC first.`);
|
||||
try {
|
||||
await config.refreshAuth(AuthType.COMPUTE_ADC);
|
||||
logger.info(`[${logPrefix}] COMPUTE_ADC successful.`);
|
||||
} catch (adcError) {
|
||||
const adcMessage =
|
||||
adcError instanceof Error ? adcError.message : String(adcError);
|
||||
logger.info(
|
||||
`[${logPrefix}] COMPUTE_ADC failed or not available: ${adcMessage}`,
|
||||
if (adcFilePath) {
|
||||
path.resolve(adcFilePath);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`[${logPrefix}] USE_CCPA env var is true but unable to resolve GOOGLE_APPLICATION_CREDENTIALS file path ${adcFilePath}. Error ${e}`,
|
||||
);
|
||||
}
|
||||
|
||||
const useComputeAdc =
|
||||
process.env['GEMINI_CLI_USE_COMPUTE_ADC'] === 'true';
|
||||
const isHeadless = isHeadlessMode();
|
||||
const useComputeAdc = process.env['GEMINI_CLI_USE_COMPUTE_ADC'] === 'true';
|
||||
const isHeadless = isHeadlessMode();
|
||||
const shouldSkipOauth = isHeadless || useComputeAdc;
|
||||
|
||||
if (isHeadless || useComputeAdc) {
|
||||
const reason = isHeadless
|
||||
? 'headless mode'
|
||||
: 'GEMINI_CLI_USE_COMPUTE_ADC=true';
|
||||
if (shouldSkipOauth) {
|
||||
if (isCloudShell() || useComputeAdc) {
|
||||
logger.info(
|
||||
`[${logPrefix}] Skipping LOGIN_WITH_GOOGLE due to ${isHeadless ? 'headless mode' : 'GEMINI_CLI_USE_COMPUTE_ADC'}. Attempting COMPUTE_ADC.`,
|
||||
);
|
||||
try {
|
||||
await config.refreshAuth(AuthType.COMPUTE_ADC);
|
||||
logger.info(`[${logPrefix}] COMPUTE_ADC successful.`);
|
||||
} catch (adcError) {
|
||||
const adcMessage =
|
||||
adcError instanceof Error ? adcError.message : String(adcError);
|
||||
throw new FatalAuthenticationError(
|
||||
`COMPUTE_ADC failed: ${adcMessage}. (Skipped LOGIN_WITH_GOOGLE due to ${isHeadless ? 'headless mode' : 'GEMINI_CLI_USE_COMPUTE_ADC'})`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new FatalAuthenticationError(
|
||||
`COMPUTE_ADC failed: ${adcMessage}. (LOGIN_WITH_GOOGLE fallback skipped due to ${reason}. Run in an interactive terminal to use OAuth.)`,
|
||||
`Interactive terminal required for LOGIN_WITH_GOOGLE. Run in an interactive terminal or set GEMINI_CLI_USE_COMPUTE_ADC=true to use Application Default Credentials.`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${logPrefix}] COMPUTE_ADC failed, falling back to LOGIN_WITH_GOOGLE.`,
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
|
||||
} catch (e) {
|
||||
if (e instanceof FatalAuthenticationError) {
|
||||
const originalMessage = e instanceof Error ? e.message : String(e);
|
||||
throw new FatalAuthenticationError(
|
||||
`${originalMessage}. The initial COMPUTE_ADC attempt also failed: ${adcMessage}`,
|
||||
if (
|
||||
e instanceof FatalAuthenticationError &&
|
||||
(isCloudShell() || useComputeAdc)
|
||||
) {
|
||||
logger.warn(
|
||||
`[${logPrefix}] LOGIN_WITH_GOOGLE failed. Attempting COMPUTE_ADC fallback.`,
|
||||
);
|
||||
try {
|
||||
await config.refreshAuth(AuthType.COMPUTE_ADC);
|
||||
logger.info(`[${logPrefix}] COMPUTE_ADC fallback successful.`);
|
||||
} catch (adcError) {
|
||||
logger.error(
|
||||
`[${logPrefix}] COMPUTE_ADC fallback failed: ${adcError}`,
|
||||
);
|
||||
const originalMessage = e instanceof Error ? e.message : String(e);
|
||||
const adcMessage =
|
||||
adcError instanceof Error ? adcError.message : String(adcError);
|
||||
throw new FatalAuthenticationError(
|
||||
`${originalMessage}. Fallback to COMPUTE_ADC also failed: ${adcMessage}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${logPrefix}] GOOGLE_CLOUD_PROJECT: ${process.env['GOOGLE_CLOUD_PROJECT']}`,
|
||||
);
|
||||
|
||||
@@ -109,8 +109,12 @@ export function createMockConfig(
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
},
|
||||
}),
|
||||
isAutoDistillationEnabled: vi.fn().mockReturnValue(false),
|
||||
getContextManagementConfig: vi.fn().mockReturnValue({ enabled: false }),
|
||||
isExperimentalAgentHistoryTruncationEnabled: vi.fn().mockReturnValue(false),
|
||||
getExperimentalAgentHistoryTruncationThreshold: vi.fn().mockReturnValue(50),
|
||||
getExperimentalAgentHistoryRetainedMessages: vi.fn().mockReturnValue(30),
|
||||
isExperimentalAgentHistorySummarizationEnabled: vi
|
||||
.fn()
|
||||
.mockReturnValue(false),
|
||||
...overrides,
|
||||
} as unknown as Config;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^12.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink": "npm:@jrichman/ink@6.5.0",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
type MessageBus,
|
||||
LlmRole,
|
||||
type GitService,
|
||||
type ModelRouterService,
|
||||
processSingleFileContent,
|
||||
InvalidStreamError,
|
||||
} from '@google/gemini-cli-core';
|
||||
@@ -103,7 +102,17 @@ vi.mock(
|
||||
...actual,
|
||||
updatePolicy: vi.fn(),
|
||||
createPolicyUpdater: vi.fn(),
|
||||
ReadManyFilesTool: vi.fn(),
|
||||
ReadManyFilesTool: vi.fn().mockImplementation(() => ({
|
||||
name: 'read_many_files',
|
||||
kind: 'read',
|
||||
build: vi.fn().mockReturnValue({
|
||||
getDescription: () => 'Read files',
|
||||
toolLocations: () => [],
|
||||
execute: vi.fn().mockResolvedValue({
|
||||
llmContent: ['--- file.txt ---\n\nFile content\n\n'],
|
||||
}),
|
||||
}),
|
||||
})),
|
||||
logToolCall: vi.fn(),
|
||||
LlmRole: {
|
||||
MAIN: 'main',
|
||||
@@ -412,26 +421,6 @@ describe('GeminiAgent', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should include gemini-3.1-flash-lite when useGemini31FlashLite is true', async () => {
|
||||
mockConfig.getHasAccessToPreviewModel = vi.fn().mockReturnValue(true);
|
||||
mockConfig.getGemini31LaunchedSync = vi.fn().mockReturnValue(true);
|
||||
mockConfig.getGemini31FlashLiteLaunchedSync = vi.fn().mockReturnValue(true);
|
||||
|
||||
const response = await agent.newSession({
|
||||
cwd: '/tmp',
|
||||
mcpServers: [],
|
||||
});
|
||||
|
||||
expect(response.models?.availableModels).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
modelId: 'gemini-3.1-flash-lite-preview',
|
||||
name: 'gemini-3.1-flash-lite-preview',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return modes with plan mode when plan is enabled', async () => {
|
||||
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
|
||||
apiKey: 'test-key',
|
||||
@@ -657,7 +646,6 @@ describe('Session', () => {
|
||||
sendMessageStream: vi.fn(),
|
||||
addHistory: vi.fn(),
|
||||
recordCompletedToolCalls: vi.fn(),
|
||||
getHistory: vi.fn().mockReturnValue([]),
|
||||
} as unknown as Mocked<GeminiChat>;
|
||||
mockTool = {
|
||||
kind: 'read',
|
||||
@@ -679,9 +667,6 @@ describe('Session', () => {
|
||||
mockConfig = {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getActiveModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getModelRouterService: vi.fn().mockReturnValue({
|
||||
route: vi.fn().mockResolvedValue({ model: 'resolved-model' }),
|
||||
}),
|
||||
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
|
||||
getMcpServers: vi.fn(),
|
||||
getFileService: vi.fn().mockReturnValue({
|
||||
@@ -728,22 +713,10 @@ describe('Session', () => {
|
||||
},
|
||||
errors: [],
|
||||
} as unknown as LoadedSettings);
|
||||
|
||||
(ReadManyFilesTool as unknown as Mock).mockImplementation(() => ({
|
||||
name: 'read_many_files',
|
||||
kind: 'read',
|
||||
build: vi.fn().mockReturnValue({
|
||||
getDescription: () => 'Read files',
|
||||
toolLocations: () => [],
|
||||
execute: vi.fn().mockResolvedValue({
|
||||
llmContent: ['--- file.txt ---\n\nFile content\n\n'],
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should send available commands', async () => {
|
||||
@@ -813,42 +786,6 @@ describe('Session', () => {
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should use model router to determine model', async () => {
|
||||
const mockRouter = {
|
||||
route: vi.fn().mockResolvedValue({ model: 'routed-model' }),
|
||||
} as unknown as ModelRouterService;
|
||||
mockConfig.getModelRouterService.mockReturnValue(mockRouter);
|
||||
|
||||
const stream = createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: {
|
||||
candidates: [{ content: { parts: [{ text: 'Hello' }] } }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
mockChat.sendMessageStream.mockResolvedValue(stream);
|
||||
|
||||
await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [{ type: 'text', text: 'Hi' }],
|
||||
});
|
||||
|
||||
expect(mockRouter.route).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requestedModel: 'gemini-pro',
|
||||
request: [{ text: 'Hi' }],
|
||||
}),
|
||||
);
|
||||
expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ model: 'routed-model' }),
|
||||
expect.any(Array),
|
||||
expect.any(String),
|
||||
expect.any(Object),
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle prompt with empty response (InvalidStreamError)', async () => {
|
||||
mockChat.sendMessageStream.mockRejectedValue(
|
||||
new InvalidStreamError('Empty response', 'NO_RESPONSE_TEXT'),
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
debugLogger,
|
||||
ReadManyFilesTool,
|
||||
REFERENCE_CONTENT_START,
|
||||
type RoutingContext,
|
||||
resolveModel,
|
||||
createWorkingStdio,
|
||||
startupProfiler,
|
||||
Kind,
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
@@ -759,15 +758,10 @@ export class Session {
|
||||
const functionCalls: FunctionCall[] = [];
|
||||
|
||||
try {
|
||||
const routingContext: RoutingContext = {
|
||||
history: chat.getHistory(/*curated=*/ true),
|
||||
request: nextMessage?.parts ?? [],
|
||||
signal: pendingSend.signal,
|
||||
requestedModel: this.context.config.getModel(),
|
||||
};
|
||||
|
||||
const router = this.context.config.getModelRouterService();
|
||||
const { model } = await router.route(routingContext);
|
||||
const model = resolveModel(
|
||||
this.context.config.getModel(),
|
||||
(await this.context.config.getGemini31Launched?.()) ?? false,
|
||||
);
|
||||
const responseStream = await chat.sendMessageStream(
|
||||
{ model },
|
||||
nextMessage?.parts ?? [],
|
||||
@@ -2015,31 +2009,10 @@ function buildAvailableModels(
|
||||
const preferredModel = config.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
|
||||
const shouldShowPreviewModels = config.getHasAccessToPreviewModel();
|
||||
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
|
||||
const useGemini31FlashLite =
|
||||
config.getGemini31FlashLiteLaunchedSync?.() ?? false;
|
||||
const selectedAuthType = settings.merged.security.auth.selectedType;
|
||||
const useCustomToolModel =
|
||||
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
|
||||
|
||||
// --- DYNAMIC PATH ---
|
||||
if (
|
||||
config.getExperimentalDynamicModelConfiguration?.() === true &&
|
||||
config.getModelConfigService
|
||||
) {
|
||||
const options = config.getModelConfigService().getAvailableModelOptions({
|
||||
useGemini3_1: useGemini31,
|
||||
useGemini3_1FlashLite: useGemini31FlashLite,
|
||||
useCustomTools: useCustomToolModel,
|
||||
hasAccessToPreview: shouldShowPreviewModels,
|
||||
});
|
||||
|
||||
return {
|
||||
availableModels: options,
|
||||
currentModelId: preferredModel,
|
||||
};
|
||||
}
|
||||
|
||||
// --- LEGACY PATH ---
|
||||
const mainOptions = [
|
||||
{
|
||||
value: DEFAULT_GEMINI_MODEL_AUTO,
|
||||
@@ -2083,7 +2056,7 @@ function buildAvailableModels(
|
||||
? PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
|
||||
: previewProModel;
|
||||
|
||||
const previewOptions = [
|
||||
manualOptions.unshift(
|
||||
{
|
||||
value: previewProValue,
|
||||
title: getDisplayString(previewProModel),
|
||||
@@ -2092,16 +2065,7 @@ function buildAvailableModels(
|
||||
value: PREVIEW_GEMINI_FLASH_MODEL,
|
||||
title: getDisplayString(PREVIEW_GEMINI_FLASH_MODEL),
|
||||
},
|
||||
];
|
||||
|
||||
if (useGemini31FlashLite) {
|
||||
previewOptions.push({
|
||||
value: PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
|
||||
title: getDisplayString(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL),
|
||||
});
|
||||
}
|
||||
|
||||
manualOptions.unshift(...previewOptions);
|
||||
);
|
||||
}
|
||||
|
||||
const scaleOptions = (
|
||||
|
||||
@@ -284,7 +284,7 @@ export class LinkExtensionCommand implements Command {
|
||||
|
||||
try {
|
||||
await stat(sourceFilepath);
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
return { name: this.name, data: `Invalid source: ${sourceFilepath}` };
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ export class ListCheckpointsCommand implements Command {
|
||||
const checkpointDir = config.storage.getProjectTempCheckpointsDir();
|
||||
try {
|
||||
await fs.mkdir(checkpointDir, { recursive: true });
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ export class ListCheckpointsCommand implements Command {
|
||||
name: this.name,
|
||||
data: `Available Checkpoints:\n${formatted}`,
|
||||
};
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
return {
|
||||
name: this.name,
|
||||
data: 'An unexpected error occurred while listing checkpoints.',
|
||||
|
||||
@@ -25,7 +25,7 @@ async function pathExists(path: string) {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('mcp command', () => {
|
||||
|
||||
try {
|
||||
await parser.parse('mcp');
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
// yargs might throw an error when demandCommand is not met
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ async function testMCPConnection(
|
||||
try {
|
||||
// Use the same transport creation logic as core
|
||||
transport = await createTransport(serverName, config, false, mcpContext);
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
await client.close();
|
||||
return MCPServerStatus.DISCONNECTED;
|
||||
}
|
||||
@@ -135,7 +135,7 @@ async function testMCPConnection(
|
||||
|
||||
await client.close();
|
||||
return MCPServerStatus.CONNECTED;
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
await transport.close();
|
||||
return MCPServerStatus.DISCONNECTED;
|
||||
}
|
||||
|
||||
@@ -1364,8 +1364,8 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
'test',
|
||||
];
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
plan: { enabled: true },
|
||||
experimental: {
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
@@ -1479,7 +1479,9 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
defaultApprovalMode: 'plan',
|
||||
plan: { enabled: false },
|
||||
},
|
||||
experimental: {
|
||||
plan: false,
|
||||
},
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
@@ -1487,12 +1489,14 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should allow plan approval mode if plan is enabled', async () => {
|
||||
it('should allow plan approval mode if experimental plan is enabled', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
defaultApprovalMode: 'plan',
|
||||
plan: { enabled: true },
|
||||
},
|
||||
experimental: {
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
@@ -2738,12 +2742,12 @@ describe('loadCliConfig approval mode', () => {
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||
});
|
||||
|
||||
it('should set Plan approval mode when --approval-mode=plan is used and plan is enabled', async () => {
|
||||
it('should set Plan approval mode when --approval-mode=plan is used and experimental.plan is enabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
plan: { enabled: true },
|
||||
experimental: {
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
@@ -2763,12 +2767,12 @@ describe('loadCliConfig approval mode', () => {
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should throw error when --approval-mode=plan is used but plan is disabled', async () => {
|
||||
it('should throw error when --approval-mode=plan is used but experimental.plan is disabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
plan: { enabled: false },
|
||||
experimental: {
|
||||
plan: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2889,26 +2893,22 @@ describe('loadCliConfig approval mode', () => {
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||
});
|
||||
|
||||
it('should respect plan mode from settings when plan is enabled', async () => {
|
||||
it('should respect plan mode from settings when experimental.plan is enabled', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
defaultApprovalMode: 'plan',
|
||||
plan: { enabled: true },
|
||||
},
|
||||
general: { defaultApprovalMode: 'plan' },
|
||||
experimental: { plan: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
|
||||
});
|
||||
|
||||
it('should fall back to default if plan mode is in settings but disabled', async () => {
|
||||
it('should throw error if plan mode is in settings but experimental.plan is disabled', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
defaultApprovalMode: 'plan',
|
||||
plan: { enabled: false },
|
||||
},
|
||||
general: { defaultApprovalMode: 'plan' },
|
||||
experimental: { plan: false },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
@@ -3696,9 +3696,7 @@ describe('loadCliConfig mcpEnabled', () => {
|
||||
it('should use plan directory from active extension when user has not specified one', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
plan: { enabled: true },
|
||||
},
|
||||
experimental: { plan: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
|
||||
@@ -3717,11 +3715,9 @@ describe('loadCliConfig mcpEnabled', () => {
|
||||
it('should NOT use plan directory from active extension when user has specified one', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: { plan: true },
|
||||
general: {
|
||||
plan: {
|
||||
enabled: true,
|
||||
directory: 'user-plans-dir',
|
||||
},
|
||||
plan: { directory: 'user-plans-dir' },
|
||||
},
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
@@ -3742,9 +3738,7 @@ describe('loadCliConfig mcpEnabled', () => {
|
||||
it('should NOT use plan directory from inactive extension', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
plan: { enabled: true },
|
||||
},
|
||||
experimental: { plan: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
|
||||
@@ -3765,9 +3759,7 @@ describe('loadCliConfig mcpEnabled', () => {
|
||||
it('should use default path if neither user nor extension settings provide a plan directory', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
plan: { enabled: true },
|
||||
},
|
||||
experimental: { plan: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
|
||||
|
||||
@@ -669,9 +669,9 @@ export async function loadCliConfig(
|
||||
approvalMode = ApprovalMode.AUTO_EDIT;
|
||||
break;
|
||||
case 'plan':
|
||||
if (!(settings.general?.plan?.enabled ?? true)) {
|
||||
if (!(settings.experimental?.plan ?? false)) {
|
||||
debugLogger.warn(
|
||||
'Approval mode "plan" is disabled in your settings. Falling back to "default".',
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled. Falling back to "default".',
|
||||
);
|
||||
approvalMode = ApprovalMode.DEFAULT;
|
||||
} else {
|
||||
@@ -966,7 +966,7 @@ export async function loadCliConfig(
|
||||
extensionRegistryURI,
|
||||
enableExtensionReloading: settings.experimental?.extensionReloading,
|
||||
enableAgents: settings.experimental?.enableAgents,
|
||||
plan: settings.general?.plan?.enabled ?? true,
|
||||
plan: settings.experimental?.plan,
|
||||
tracker: settings.experimental?.taskTracker,
|
||||
directWebFetch: settings.experimental?.directWebFetch,
|
||||
planSettings: settings.general?.plan?.directory
|
||||
@@ -977,12 +977,17 @@ export async function loadCliConfig(
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
experimentalMemoryManager: settings.experimental?.memoryManager,
|
||||
contextManagement: {
|
||||
enabled: settings.experimental?.contextManagement,
|
||||
...settings?.contextManagement,
|
||||
},
|
||||
experimentalAgentHistoryTruncation:
|
||||
settings.experimental?.agentHistoryTruncation,
|
||||
experimentalAgentHistoryTruncationThreshold:
|
||||
settings.experimental?.agentHistoryTruncationThreshold,
|
||||
experimentalAgentHistoryRetainedMessages:
|
||||
settings.experimental?.agentHistoryRetainedMessages,
|
||||
experimentalAgentHistorySummarization:
|
||||
settings.experimental?.agentHistorySummarization,
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
|
||||
toolOutputMasking: settings.experimental?.toolOutputMasking,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
ideMode,
|
||||
@@ -995,8 +1000,6 @@ export async function loadCliConfig(
|
||||
useAlternateBuffer: settings.ui?.useAlternateBuffer,
|
||||
useRipgrep: settings.tools?.useRipgrep,
|
||||
enableInteractiveShell: settings.tools?.shell?.enableInteractiveShell,
|
||||
shellBackgroundCompletionBehavior: settings.tools?.shell
|
||||
?.backgroundCompletionBehavior as string | undefined,
|
||||
shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout,
|
||||
enableShellOutputEfficiency:
|
||||
settings.tools?.shell?.enableShellOutputEfficiency ?? true,
|
||||
@@ -1009,7 +1012,6 @@ export async function loadCliConfig(
|
||||
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
|
||||
},
|
||||
gemmaModelRouter: settings.experimental?.gemmaModelRouter,
|
||||
adk: settings.experimental?.adk,
|
||||
fakeResponses: argv.fakeResponses,
|
||||
recordResponses: argv.recordResponses,
|
||||
retryFetchErrors: settings.general?.retryFetchErrors,
|
||||
@@ -1064,7 +1066,7 @@ async function resolveWorktreeSettings(
|
||||
if (isGeminiWorktree(toplevel, projectRoot)) {
|
||||
worktreePath = toplevel;
|
||||
}
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('copyExtension permissions', () => {
|
||||
makeWritableSync(path.join(p, child)),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
};
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('ExtensionManager', () => {
|
||||
themeManager.clearExtensionThemes();
|
||||
try {
|
||||
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
// Ignore
|
||||
}
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ export function loadInstallMetadata(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const metadata = JSON.parse(configContent) as ExtensionInstallMetadata;
|
||||
return metadata;
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ export async function fetchReleaseFromGithub(
|
||||
return await fetchJson(
|
||||
`https://api.github.com/repos/${owner}/${repo}/releases/latest`,
|
||||
);
|
||||
} catch {
|
||||
} catch (_) {
|
||||
// This can fail if there is no release marked latest. In that case
|
||||
// we want to just try the pre-release logic below.
|
||||
}
|
||||
|
||||
@@ -5,153 +5,87 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
deriveItemsFromLegacySettings,
|
||||
resolveFooterState,
|
||||
} from './footerItems.js';
|
||||
import { deriveItemsFromLegacySettings } from './footerItems.js';
|
||||
import { createMockSettings } from '../test-utils/settings.js';
|
||||
|
||||
describe('footerItems', () => {
|
||||
describe('deriveItemsFromLegacySettings', () => {
|
||||
it('returns defaults when no legacy settings are customized', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toEqual([
|
||||
'workspace',
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'model-name',
|
||||
'quota',
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes workspace when hideCWD is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideCWD: true, hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).not.toContain('workspace');
|
||||
});
|
||||
|
||||
it('removes sandbox when hideSandboxStatus is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: {
|
||||
footer: { hideSandboxStatus: true, hideContextPercentage: true },
|
||||
},
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).not.toContain('sandbox');
|
||||
});
|
||||
|
||||
it('removes model-name, context-used, and quota when hideModelInfo is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideModelInfo: true, hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).not.toContain('model-name');
|
||||
expect(items).not.toContain('context-used');
|
||||
expect(items).not.toContain('quota');
|
||||
});
|
||||
|
||||
it('includes context-used when hideContextPercentage is false', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideContextPercentage: false } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toContain('context-used');
|
||||
// Should be after model-name
|
||||
const modelIdx = items.indexOf('model-name');
|
||||
const contextIdx = items.indexOf('context-used');
|
||||
expect(contextIdx).toBe(modelIdx + 1);
|
||||
});
|
||||
|
||||
it('includes memory-usage when showMemoryUsage is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { showMemoryUsage: true, footer: { hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toContain('memory-usage');
|
||||
});
|
||||
|
||||
it('handles combination of settings', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: {
|
||||
showMemoryUsage: true,
|
||||
footer: {
|
||||
hideCWD: true,
|
||||
hideModelInfo: true,
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
},
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toEqual([
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'context-used',
|
||||
'memory-usage',
|
||||
]);
|
||||
});
|
||||
describe('deriveItemsFromLegacySettings', () => {
|
||||
it('returns defaults when no legacy settings are customized', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toEqual([
|
||||
'workspace',
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'model-name',
|
||||
'quota',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('resolveFooterState', () => {
|
||||
it('filters out auth item when showUserIdentity is false', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: {
|
||||
showUserIdentity: false,
|
||||
footer: {
|
||||
items: ['workspace', 'auth', 'model-name'],
|
||||
},
|
||||
it('removes workspace when hideCWD is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideCWD: true, hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).not.toContain('workspace');
|
||||
});
|
||||
|
||||
it('removes sandbox when hideSandboxStatus is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideSandboxStatus: true, hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).not.toContain('sandbox');
|
||||
});
|
||||
|
||||
it('removes model-name, context-used, and quota when hideModelInfo is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideModelInfo: true, hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).not.toContain('model-name');
|
||||
expect(items).not.toContain('context-used');
|
||||
expect(items).not.toContain('quota');
|
||||
});
|
||||
|
||||
it('includes context-used when hideContextPercentage is false', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { footer: { hideContextPercentage: false } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toContain('context-used');
|
||||
// Should be after model-name
|
||||
const modelIdx = items.indexOf('model-name');
|
||||
const contextIdx = items.indexOf('context-used');
|
||||
expect(contextIdx).toBe(modelIdx + 1);
|
||||
});
|
||||
|
||||
it('includes memory-usage when showMemoryUsage is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { showMemoryUsage: true, footer: { hideContextPercentage: true } },
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toContain('memory-usage');
|
||||
});
|
||||
|
||||
it('handles combination of settings', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: {
|
||||
showMemoryUsage: true,
|
||||
footer: {
|
||||
hideCWD: true,
|
||||
hideModelInfo: true,
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
}).merged;
|
||||
|
||||
const state = resolveFooterState(settings);
|
||||
expect(state.orderedIds).not.toContain('auth');
|
||||
expect(state.selectedIds.has('auth')).toBe(false);
|
||||
// It should also not be in the 'others' part of orderedIds
|
||||
expect(state.orderedIds).toEqual([
|
||||
'workspace',
|
||||
'model-name',
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'context-used',
|
||||
'quota',
|
||||
'memory-usage',
|
||||
'session-id',
|
||||
'code-changes',
|
||||
'token-count',
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes auth item when showUserIdentity is true', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: {
|
||||
showUserIdentity: true,
|
||||
footer: {
|
||||
items: ['workspace', 'auth', 'model-name'],
|
||||
},
|
||||
},
|
||||
}).merged;
|
||||
|
||||
const state = resolveFooterState(settings);
|
||||
expect(state.orderedIds).toContain('auth');
|
||||
expect(state.selectedIds.has('auth')).toBe(true);
|
||||
});
|
||||
|
||||
it('includes auth item by default when showUserIdentity is undefined (defaults to true)', () => {
|
||||
const settings = createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['workspace', 'auth', 'model-name'],
|
||||
},
|
||||
},
|
||||
}).merged;
|
||||
|
||||
const state = resolveFooterState(settings);
|
||||
expect(state.orderedIds).toContain('auth');
|
||||
expect(state.selectedIds.has('auth')).toBe(true);
|
||||
});
|
||||
},
|
||||
}).merged;
|
||||
const items = deriveItemsFromLegacySettings(settings);
|
||||
expect(items).toEqual([
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'context-used',
|
||||
'memory-usage',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,11 +47,6 @@ export const ALL_ITEMS = [
|
||||
header: 'session',
|
||||
description: 'Unique identifier for the current session',
|
||||
},
|
||||
{
|
||||
id: 'auth',
|
||||
header: '/auth',
|
||||
description: 'Current authentication info',
|
||||
},
|
||||
{
|
||||
id: 'code-changes',
|
||||
header: 'diff',
|
||||
@@ -75,7 +70,6 @@ export const DEFAULT_ORDER = [
|
||||
'quota',
|
||||
'memory-usage',
|
||||
'session-id',
|
||||
'auth',
|
||||
'code-changes',
|
||||
'token-count',
|
||||
];
|
||||
@@ -127,19 +121,10 @@ export function resolveFooterState(settings: MergedSettings): {
|
||||
orderedIds: string[];
|
||||
selectedIds: Set<string>;
|
||||
} {
|
||||
const showUserIdentity = settings.ui?.showUserIdentity !== false;
|
||||
const filteredValidIds = showUserIdentity
|
||||
? VALID_IDS
|
||||
: new Set([...VALID_IDS].filter((id) => id !== 'auth'));
|
||||
|
||||
const source = (
|
||||
settings.ui?.footer?.items ?? deriveItemsFromLegacySettings(settings)
|
||||
).filter((id: string) => filteredValidIds.has(id));
|
||||
|
||||
const others = DEFAULT_ORDER.filter(
|
||||
(id) => !source.includes(id) && filteredValidIds.has(id),
|
||||
);
|
||||
|
||||
).filter((id: string) => VALID_IDS.has(id));
|
||||
const others = DEFAULT_ORDER.filter((id) => !source.includes(id));
|
||||
return {
|
||||
orderedIds: [...source, ...others],
|
||||
selectedIds: new Set(source),
|
||||
|
||||
@@ -612,7 +612,7 @@ export function loadEnvironment(
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
// Errors are ignored to match the behavior of `dotenv.config({ quiet: true })`.
|
||||
}
|
||||
}
|
||||
@@ -1124,15 +1124,15 @@ function migrateExperimentalSettings(
|
||||
};
|
||||
let modified = false;
|
||||
|
||||
const migrateExperimental = <T = Record<string, unknown>>(
|
||||
const migrateExperimental = (
|
||||
oldKey: string,
|
||||
migrateFn: (oldValue: T) => void,
|
||||
migrateFn: (oldValue: Record<string, unknown>) => void,
|
||||
) => {
|
||||
const old = experimentalSettings[oldKey];
|
||||
if (old !== undefined) {
|
||||
if (old) {
|
||||
foundDeprecated?.push(`experimental.${oldKey}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
migrateFn(old as T);
|
||||
migrateFn(old as Record<string, unknown>);
|
||||
modified = true;
|
||||
}
|
||||
};
|
||||
@@ -1197,24 +1197,6 @@ function migrateExperimentalSettings(
|
||||
agentsOverrides['cli_help'] = override;
|
||||
});
|
||||
|
||||
// Migrate experimental.plan -> general.plan.enabled
|
||||
migrateExperimental<boolean>('plan', (planValue) => {
|
||||
const generalSettings =
|
||||
(settings.general as Record<string, unknown> | undefined) || {};
|
||||
const newGeneral = { ...generalSettings };
|
||||
const planSettings =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(newGeneral['plan'] as Record<string, unknown> | undefined) || {};
|
||||
const newPlan = { ...planSettings };
|
||||
|
||||
if (newPlan['enabled'] === undefined) {
|
||||
newPlan['enabled'] = planValue;
|
||||
newGeneral['plan'] = newPlan;
|
||||
loadedSettings.setValue(scope, 'general', newGeneral);
|
||||
modified = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (modified) {
|
||||
agentsSettings['overrides'] = agentsOverrides;
|
||||
loadedSettings.setValue(scope, 'agents', agentsSettings);
|
||||
@@ -1223,7 +1205,6 @@ function migrateExperimentalSettings(
|
||||
const newExperimental = { ...experimentalSettings };
|
||||
delete newExperimental['codebaseInvestigatorSettings'];
|
||||
delete newExperimental['cliHelpAgentSettings'];
|
||||
delete newExperimental['plan'];
|
||||
loadedSettings.setValue(scope, 'experimental', newExperimental);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('SettingsSchema', () => {
|
||||
const definition = getSettingsSchema().ui?.properties?.loadingPhrases;
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition?.type).toBe('enum');
|
||||
expect(definition?.default).toBe('off');
|
||||
expect(definition?.default).toBe('tips');
|
||||
expect(definition?.options?.map((o) => o.value)).toEqual([
|
||||
'tips',
|
||||
'witty',
|
||||
@@ -418,17 +418,14 @@ describe('SettingsSchema', () => {
|
||||
});
|
||||
|
||||
it('should have plan setting in schema', () => {
|
||||
const setting =
|
||||
getSettingsSchema().general.properties.plan.properties.enabled;
|
||||
const setting = getSettingsSchema().experimental.properties.plan;
|
||||
expect(setting).toBeDefined();
|
||||
expect(setting.type).toBe('boolean');
|
||||
expect(setting.category).toBe('General');
|
||||
expect(setting.category).toBe('Experimental');
|
||||
expect(setting.default).toBe(true);
|
||||
expect(setting.requiresRestart).toBe(true);
|
||||
expect(setting.showInDialog).toBe(true);
|
||||
expect(setting.description).toBe(
|
||||
'Enable Plan Mode for read-only safety during planning.',
|
||||
);
|
||||
expect(setting.description).toBe('Enable Plan Mode.');
|
||||
});
|
||||
|
||||
it('should have hooksConfig.notifications setting in schema', () => {
|
||||
@@ -505,31 +502,6 @@ describe('SettingsSchema', () => {
|
||||
'The model to use for the classifier. Only tested on `gemma3-1b-gpu-custom`.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should have adk setting in schema', () => {
|
||||
const adk = getSettingsSchema().experimental.properties.adk;
|
||||
expect(adk).toBeDefined();
|
||||
expect(adk.type).toBe('object');
|
||||
expect(adk.category).toBe('Experimental');
|
||||
expect(adk.default).toEqual({});
|
||||
expect(adk.requiresRestart).toBe(true);
|
||||
expect(adk.showInDialog).toBe(false);
|
||||
expect(adk.description).toBe(
|
||||
'Settings for the Agent Development Kit (ADK).',
|
||||
);
|
||||
|
||||
const agentSessionNoninteractiveEnabled =
|
||||
adk.properties.agentSessionNoninteractiveEnabled;
|
||||
expect(agentSessionNoninteractiveEnabled).toBeDefined();
|
||||
expect(agentSessionNoninteractiveEnabled.type).toBe('boolean');
|
||||
expect(agentSessionNoninteractiveEnabled.category).toBe('Experimental');
|
||||
expect(agentSessionNoninteractiveEnabled.default).toBe(false);
|
||||
expect(agentSessionNoninteractiveEnabled.requiresRestart).toBe(true);
|
||||
expect(agentSessionNoninteractiveEnabled.showInDialog).toBe(false);
|
||||
expect(agentSessionNoninteractiveEnabled.description).toBe(
|
||||
'Enable non-interactive agent sessions.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('has JSON schema definitions for every referenced ref', () => {
|
||||
|
||||
@@ -293,16 +293,6 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Planning features configuration.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Plan Mode',
|
||||
category: 'General',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description:
|
||||
'Enable Plan Mode for read-only safety during planning.',
|
||||
showInDialog: true,
|
||||
},
|
||||
directory: {
|
||||
type: 'string',
|
||||
label: 'Plan Directory',
|
||||
@@ -571,16 +561,6 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Show the "? for shortcuts" hint above the input.',
|
||||
showInDialog: true,
|
||||
},
|
||||
compactToolOutput: {
|
||||
type: 'boolean',
|
||||
label: 'Compact Tool Output',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Display tool outputs (like directory listings and file reads) in a compact, structured format.',
|
||||
showInDialog: true,
|
||||
},
|
||||
hideBanner: {
|
||||
type: 'boolean',
|
||||
label: 'Hide Banner',
|
||||
@@ -776,9 +756,9 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'Loading Phrases',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: 'off',
|
||||
default: 'tips',
|
||||
description:
|
||||
'What to show while the model is working: tips, witty comments, all, or off.',
|
||||
'What to show while the model is working: tips, witty comments, both, or nothing.',
|
||||
showInDialog: true,
|
||||
options: [
|
||||
{ value: 'tips', label: 'Tips' },
|
||||
@@ -1478,21 +1458,6 @@ const SETTINGS_SCHEMA = {
|
||||
`,
|
||||
showInDialog: true,
|
||||
},
|
||||
backgroundCompletionBehavior: {
|
||||
type: 'enum',
|
||||
label: 'Background Completion Behavior',
|
||||
category: 'Tools',
|
||||
requiresRestart: false,
|
||||
default: 'silent',
|
||||
description:
|
||||
"Controls what happens when a background shell command finishes. 'silent' (default): quietly exits in background. 'inject': automatically returns output to agent. 'notify': shows brief message in chat.",
|
||||
showInDialog: false,
|
||||
options: [
|
||||
{ label: 'Silent', value: 'silent' },
|
||||
{ label: 'Inject', value: 'inject' },
|
||||
{ label: 'Notify', value: 'notify' },
|
||||
],
|
||||
},
|
||||
pager: {
|
||||
type: 'string',
|
||||
label: 'Pager',
|
||||
@@ -1887,7 +1852,7 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'Auto Configure Max Old Space Size',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
default: false,
|
||||
description: 'Automatically configure Node.js memory limits',
|
||||
showInDialog: true,
|
||||
},
|
||||
@@ -1933,22 +1898,54 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Setting to enable experimental features',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
adk: {
|
||||
toolOutputMasking: {
|
||||
type: 'object',
|
||||
label: 'ADK',
|
||||
label: 'Tool Output Masking',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
ignoreInDocs: false,
|
||||
default: {},
|
||||
description: 'Settings for the Agent Development Kit (ADK).',
|
||||
description:
|
||||
'Advanced settings for tool output masking to manage context window efficiency.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
agentSessionNoninteractiveEnabled: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'Agent Session Non-interactive Enabled',
|
||||
label: 'Enable Tool Output Masking',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable non-interactive agent sessions.',
|
||||
default: true,
|
||||
description: 'Enables tool output masking to save tokens.',
|
||||
showInDialog: true,
|
||||
},
|
||||
toolProtectionThreshold: {
|
||||
type: 'number',
|
||||
label: 'Tool Protection Threshold',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: 50000,
|
||||
description:
|
||||
'Minimum number of tokens to protect from masking (most recent tool outputs).',
|
||||
showInDialog: false,
|
||||
},
|
||||
minPrunableTokensThreshold: {
|
||||
type: 'number',
|
||||
label: 'Min Prunable Tokens Threshold',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: 30000,
|
||||
description:
|
||||
'Minimum prunable tokens required to trigger a masking pass.',
|
||||
showInDialog: false,
|
||||
},
|
||||
protectLatestTurn: {
|
||||
type: 'boolean',
|
||||
label: 'Protect Latest Turn',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description:
|
||||
'Ensures the absolute latest turn is never masked, regardless of token count.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
@@ -2024,7 +2021,7 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'JIT Context Loading',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
default: true,
|
||||
description: 'Enable Just-In-Time (JIT) context loading.',
|
||||
showInDialog: false,
|
||||
},
|
||||
@@ -2048,6 +2045,15 @@ const SETTINGS_SCHEMA = {
|
||||
'Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).',
|
||||
showInDialog: true,
|
||||
},
|
||||
plan: {
|
||||
type: 'boolean',
|
||||
label: 'Plan',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable Plan Mode.',
|
||||
showInDialog: true,
|
||||
},
|
||||
taskTracker: {
|
||||
type: 'boolean',
|
||||
label: 'Task Tracker',
|
||||
@@ -2148,13 +2154,44 @@ const SETTINGS_SCHEMA = {
|
||||
'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.',
|
||||
showInDialog: true,
|
||||
},
|
||||
contextManagement: {
|
||||
agentHistoryTruncation: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Context Management',
|
||||
label: 'Agent History Truncation',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable logic for context management.',
|
||||
description:
|
||||
'Enable truncation window logic for the Agent History Provider.',
|
||||
showInDialog: true,
|
||||
},
|
||||
agentHistoryTruncationThreshold: {
|
||||
type: 'number',
|
||||
label: 'Agent History Truncation Threshold',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: 30,
|
||||
description:
|
||||
'The maximum number of messages before history is truncated.',
|
||||
showInDialog: true,
|
||||
},
|
||||
agentHistoryRetainedMessages: {
|
||||
type: 'number',
|
||||
label: 'Agent History Retained Messages',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: 15,
|
||||
description:
|
||||
'The number of recent messages to retain after truncation.',
|
||||
showInDialog: true,
|
||||
},
|
||||
agentHistorySummarization: {
|
||||
type: 'boolean',
|
||||
label: 'Agent History Summarization',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable summarization of truncated content via a small model for the Agent History Provider.',
|
||||
showInDialog: true,
|
||||
},
|
||||
topicUpdateNarration: {
|
||||
@@ -2433,171 +2470,6 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
|
||||
contextManagement: {
|
||||
type: 'object',
|
||||
label: 'Context Management',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description:
|
||||
'Settings for agent history and tool distillation context management.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
historyWindow: {
|
||||
type: 'object',
|
||||
label: 'History Window Settings',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
maxTokens: {
|
||||
type: 'number',
|
||||
label: 'Max Tokens',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: 150_000,
|
||||
description:
|
||||
'The number of tokens to allow before triggering compression.',
|
||||
showInDialog: false,
|
||||
},
|
||||
retainedTokens: {
|
||||
type: 'number',
|
||||
label: 'Retained Tokens',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: 40_000,
|
||||
description: 'The number of tokens to always retain.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
messageLimits: {
|
||||
type: 'object',
|
||||
label: 'Message Limits',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
normalMaxTokens: {
|
||||
type: 'number',
|
||||
label: 'Normal Maximum Tokens',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: 2500,
|
||||
description:
|
||||
'The target number of tokens to budget for a normal conversation turn.',
|
||||
showInDialog: false,
|
||||
},
|
||||
retainedMaxTokens: {
|
||||
type: 'number',
|
||||
label: 'Retained Maximum Tokens',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: 12000,
|
||||
description:
|
||||
'The maximum number of tokens a single conversation turn can consume before truncation.',
|
||||
showInDialog: false,
|
||||
},
|
||||
normalizationHeadRatio: {
|
||||
type: 'number',
|
||||
label: 'Normalization Head Ratio',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: 0.25,
|
||||
description:
|
||||
'The ratio of tokens to retain from the beginning of a truncated message (0.0 to 1.0).',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
type: 'object',
|
||||
label: 'Context Management Tools',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
distillation: {
|
||||
type: 'object',
|
||||
label: 'Tool Distillation',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
maxOutputTokens: {
|
||||
type: 'number',
|
||||
label: 'Max Output Tokens',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: 10_000,
|
||||
description:
|
||||
'Maximum tokens to show to the model when truncating large tool outputs.',
|
||||
showInDialog: false,
|
||||
},
|
||||
summarizationThresholdTokens: {
|
||||
type: 'number',
|
||||
label: 'Tool Summarization Threshold',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: 20_000,
|
||||
description:
|
||||
'Threshold above which truncated tool outputs will be summarized by an LLM.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
outputMasking: {
|
||||
type: 'object',
|
||||
label: 'Tool Output Masking',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
ignoreInDocs: false,
|
||||
default: {},
|
||||
description:
|
||||
'Advanced settings for tool output masking to manage context window efficiency.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
protectionThresholdTokens: {
|
||||
type: 'number',
|
||||
label: 'Tool Protection Threshold (Tokens)',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: 50_000,
|
||||
description:
|
||||
'Minimum number of tokens to protect from masking (most recent tool outputs).',
|
||||
showInDialog: false,
|
||||
},
|
||||
minPrunableThresholdTokens: {
|
||||
type: 'number',
|
||||
label: 'Min Prunable Tokens Threshold',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: 30_000,
|
||||
description:
|
||||
'Minimum prunable tokens required to trigger a masking pass.',
|
||||
showInDialog: false,
|
||||
},
|
||||
protectLatestTurn: {
|
||||
type: 'boolean',
|
||||
label: 'Protect Latest Turn',
|
||||
category: 'Context Management',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description:
|
||||
'Ensures the absolute latest turn is never masked, regardless of token count.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
admin: {
|
||||
type: 'object',
|
||||
label: 'Admin',
|
||||
|
||||
@@ -46,7 +46,6 @@ import { TerminalProvider } from './ui/contexts/TerminalContext.js';
|
||||
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
|
||||
import { OverflowProvider } from './ui/contexts/OverflowContext.js';
|
||||
import { profiler } from './ui/components/DebugProfiler.js';
|
||||
import { initializeConsoleStore } from './ui/hooks/useConsoleMessages.js';
|
||||
|
||||
const SLOW_RENDER_MS = 200;
|
||||
|
||||
@@ -58,7 +57,6 @@ export async function startInteractiveUI(
|
||||
resumedSessionData: ResumedSessionData | undefined,
|
||||
initializationResult: InitializationResult,
|
||||
) {
|
||||
initializeConsoleStore();
|
||||
// Never enter Ink alternate buffer mode when screen reader mode is enabled
|
||||
// as there is no benefit of alternate buffer mode when using a screen reader
|
||||
// and the Ink alternate buffer mode requires line wrapping harmful to
|
||||
|
||||
@@ -1712,7 +1712,7 @@ describe('runNonInteractive', () => {
|
||||
input,
|
||||
prompt_id: promptId,
|
||||
});
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
// Expected exit
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ import { themeCommand } from '../ui/commands/themeCommand.js';
|
||||
import { toolsCommand } from '../ui/commands/toolsCommand.js';
|
||||
import { skillsCommand } from '../ui/commands/skillsCommand.js';
|
||||
import { settingsCommand } from '../ui/commands/settingsCommand.js';
|
||||
import { tasksCommand } from '../ui/commands/tasksCommand.js';
|
||||
import { shellsCommand } from '../ui/commands/shellsCommand.js';
|
||||
import { vimCommand } from '../ui/commands/vimCommand.js';
|
||||
import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
|
||||
import { terminalSetupCommand } from '../ui/commands/terminalSetupCommand.js';
|
||||
@@ -221,7 +221,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
: [skillsCommand]
|
||||
: []),
|
||||
settingsCommand,
|
||||
tasksCommand,
|
||||
shellsCommand,
|
||||
vimCommand,
|
||||
setupGithubCommand,
|
||||
terminalSetupCommand,
|
||||
|
||||
@@ -61,7 +61,6 @@ export const createMockCommandContext = (
|
||||
toggleCorgiMode: vi.fn(),
|
||||
toggleShortcutsHelp: vi.fn(),
|
||||
toggleVimEnabled: vi.fn(),
|
||||
reloadCommands: vi.fn(),
|
||||
openAgentConfigDialog: vi.fn(),
|
||||
closeAgentConfigDialog: vi.fn(),
|
||||
extensionsUpdateState: new Map(),
|
||||
|
||||
@@ -38,7 +38,6 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
|
||||
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
isMemoryManagerEnabled: vi.fn(() => false),
|
||||
getListExtensions: vi.fn(() => false),
|
||||
getExtensions: vi.fn(() => []),
|
||||
getListSessions: vi.fn(() => false),
|
||||
@@ -195,17 +194,6 @@ export function createMockSettings(
|
||||
user: { settings: {} },
|
||||
workspace: { settings: {} },
|
||||
errors: [],
|
||||
subscribe: vi.fn().mockReturnValue(() => {}),
|
||||
getSnapshot: vi.fn().mockReturnValue({
|
||||
system: { settings: {} },
|
||||
systemDefaults: { settings: {} },
|
||||
user: { settings: {} },
|
||||
workspace: { settings: {} },
|
||||
isTrusted: true,
|
||||
errors: [],
|
||||
merged,
|
||||
}),
|
||||
setValue: vi.fn(),
|
||||
...overrides,
|
||||
merged,
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
type OverflowState,
|
||||
} from '../ui/contexts/OverflowContext.js';
|
||||
|
||||
import { makeFakeConfig } from '@google/gemini-cli-core';
|
||||
import { type Config } from '@google/gemini-cli-core';
|
||||
import { FakePersistentState } from './persistentStateFake.js';
|
||||
import { AppContext, type AppState } from '../ui/contexts/AppContext.js';
|
||||
@@ -52,6 +51,7 @@ import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
|
||||
import { DefaultLight } from '../ui/themes/builtin/light/default-light.js';
|
||||
import { pickDefaultThemeName } from '../ui/themes/theme.js';
|
||||
import { generateSvgForTerminal } from './svg.js';
|
||||
import { loadCliConfig, type CliArgs } from '../config/config.js';
|
||||
|
||||
export const persistentStateMock = new FakePersistentState();
|
||||
|
||||
@@ -506,8 +506,8 @@ const baseMockUiState = {
|
||||
cleanUiDetailsVisible: false,
|
||||
allowPlanMode: true,
|
||||
activePtyId: undefined,
|
||||
backgroundTasks: new Map(),
|
||||
backgroundTaskHeight: 0,
|
||||
backgroundShells: new Map(),
|
||||
backgroundShellHeight: 0,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: undefined,
|
||||
@@ -579,9 +579,9 @@ const mockUIActions: UIActions = {
|
||||
revealCleanUiDetailsTemporarily: vi.fn(),
|
||||
handleWarning: vi.fn(),
|
||||
setEmbeddedShellFocused: vi.fn(),
|
||||
dismissBackgroundTask: vi.fn(),
|
||||
setActiveBackgroundTaskPid: vi.fn(),
|
||||
setIsBackgroundTaskListOpen: vi.fn(),
|
||||
dismissBackgroundShell: vi.fn(),
|
||||
setActiveBackgroundShellPid: vi.fn(),
|
||||
setIsBackgroundShellListOpen: vi.fn(),
|
||||
setAuthContext: vi.fn(),
|
||||
onHintInput: vi.fn(),
|
||||
onHintBackspace: vi.fn(),
|
||||
@@ -613,7 +613,6 @@ export const renderWithProviders = async (
|
||||
mouseEventsEnabled = false,
|
||||
config,
|
||||
uiActions,
|
||||
toolActions,
|
||||
persistentState,
|
||||
appState = mockAppState,
|
||||
}: {
|
||||
@@ -624,11 +623,6 @@ export const renderWithProviders = async (
|
||||
mouseEventsEnabled?: boolean;
|
||||
config?: Config;
|
||||
uiActions?: Partial<UIActions>;
|
||||
toolActions?: Partial<{
|
||||
isExpanded: (callId: string) => boolean;
|
||||
toggleExpansion: (callId: string) => void;
|
||||
toggleAllExpansion: (callIds: string[]) => void;
|
||||
}>;
|
||||
persistentState?: {
|
||||
get?: typeof persistentStateMock.get;
|
||||
set?: typeof persistentStateMock.set;
|
||||
@@ -666,11 +660,12 @@ export const renderWithProviders = async (
|
||||
const terminalWidth = width ?? baseState.terminalWidth;
|
||||
|
||||
if (!config) {
|
||||
config = makeFakeConfig({
|
||||
useAlternateBuffer: settings.merged.ui?.useAlternateBuffer,
|
||||
showMemoryUsage: settings.merged.ui?.showMemoryUsage,
|
||||
accessibility: settings.merged.ui?.accessibility,
|
||||
});
|
||||
config = await loadCliConfig(
|
||||
settings.merged,
|
||||
'random-session-id',
|
||||
{} as unknown as CliArgs,
|
||||
{ cwd: '/' },
|
||||
);
|
||||
}
|
||||
|
||||
const mainAreaWidth = providedUiState?.mainAreaWidth ?? terminalWidth;
|
||||
@@ -715,16 +710,6 @@ export const renderWithProviders = async (
|
||||
<ToolActionsProvider
|
||||
config={config}
|
||||
toolCalls={allToolCalls}
|
||||
isExpanded={
|
||||
toolActions?.isExpanded ??
|
||||
vi.fn().mockReturnValue(false)
|
||||
}
|
||||
toggleExpansion={
|
||||
toolActions?.toggleExpansion ?? vi.fn()
|
||||
}
|
||||
toggleAllExpansion={
|
||||
toolActions?.toggleAllExpansion ?? vi.fn()
|
||||
}
|
||||
>
|
||||
<AskUserActionsProvider
|
||||
request={null}
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('App', () => {
|
||||
defaultText: 'Mock Banner Text',
|
||||
warningText: '',
|
||||
},
|
||||
backgroundTasks: new Map(),
|
||||
backgroundShells: new Map(),
|
||||
};
|
||||
|
||||
it('should render main content and composer when not quitting', async () => {
|
||||
|
||||
@@ -328,13 +328,13 @@ describe('AppContainer State Management', () => {
|
||||
handleApprovalModeChange: vi.fn(),
|
||||
activePtyId: null,
|
||||
loopDetectionConfirmationRequest: null,
|
||||
backgroundTaskCount: 0,
|
||||
isBackgroundTaskVisible: false,
|
||||
toggleBackgroundTasks: vi.fn(),
|
||||
backgroundCurrentExecution: vi.fn(),
|
||||
backgroundTasks: new Map(),
|
||||
registerBackgroundTask: vi.fn(),
|
||||
dismissBackgroundTask: vi.fn(),
|
||||
backgroundShellCount: 0,
|
||||
isBackgroundShellVisible: false,
|
||||
toggleBackgroundShell: vi.fn(),
|
||||
backgroundCurrentShell: vi.fn(),
|
||||
backgroundShells: new Map(),
|
||||
registerBackgroundShell: vi.fn(),
|
||||
dismissBackgroundShell: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2257,13 +2257,13 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
it('should focus background shell on Tab when already visible (not toggle it off)', async () => {
|
||||
const mockToggleBackgroundTask = vi.fn();
|
||||
const mockToggleBackgroundShell = vi.fn();
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
activePtyId: null,
|
||||
isBackgroundTaskVisible: true,
|
||||
backgroundTasks: new Map([[123, { pid: 123, status: 'running' }]]),
|
||||
toggleBackgroundTasks: mockToggleBackgroundTask,
|
||||
isBackgroundShellVisible: true,
|
||||
backgroundShells: new Map([[123, { pid: 123, status: 'running' }]]),
|
||||
toggleBackgroundShell: mockToggleBackgroundShell,
|
||||
});
|
||||
|
||||
await setupKeypressTest();
|
||||
@@ -2277,7 +2277,7 @@ describe('AppContainer State Management', () => {
|
||||
// Should be focused
|
||||
expect(capturedUIState.embeddedShellFocused).toBe(true);
|
||||
// Should NOT have toggled (closed) the shell
|
||||
expect(mockToggleBackgroundTask).not.toHaveBeenCalled();
|
||||
expect(mockToggleBackgroundShell).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
});
|
||||
@@ -2285,13 +2285,13 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
describe('Background Shell Toggling (CTRL+B)', () => {
|
||||
it('should toggle background shell on Ctrl+B even if visible but not focused', async () => {
|
||||
const mockToggleBackgroundTask = vi.fn();
|
||||
const mockToggleBackgroundShell = vi.fn();
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
activePtyId: null,
|
||||
isBackgroundTaskVisible: true,
|
||||
backgroundTasks: new Map([[123, { pid: 123, status: 'running' }]]),
|
||||
toggleBackgroundTasks: mockToggleBackgroundTask,
|
||||
isBackgroundShellVisible: true,
|
||||
backgroundShells: new Map([[123, { pid: 123, status: 'running' }]]),
|
||||
toggleBackgroundShell: mockToggleBackgroundShell,
|
||||
});
|
||||
|
||||
await setupKeypressTest();
|
||||
@@ -2303,7 +2303,7 @@ describe('AppContainer State Management', () => {
|
||||
pressKey('\x02');
|
||||
|
||||
// Should have toggled (closed) the shell
|
||||
expect(mockToggleBackgroundTask).toHaveBeenCalled();
|
||||
expect(mockToggleBackgroundShell).toHaveBeenCalled();
|
||||
// Should be unfocused
|
||||
expect(capturedUIState.embeddedShellFocused).toBe(false);
|
||||
|
||||
@@ -2311,28 +2311,28 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
it('should show and focus background shell on Ctrl+B if hidden', async () => {
|
||||
const mockToggleBackgroundTask = vi.fn();
|
||||
const mockToggleBackgroundShell = vi.fn();
|
||||
const geminiStreamMock = {
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
activePtyId: null,
|
||||
isBackgroundTaskVisible: false,
|
||||
backgroundTasks: new Map([[123, { pid: 123, status: 'running' }]]),
|
||||
toggleBackgroundTasks: mockToggleBackgroundTask,
|
||||
isBackgroundShellVisible: false,
|
||||
backgroundShells: new Map([[123, { pid: 123, status: 'running' }]]),
|
||||
toggleBackgroundShell: mockToggleBackgroundShell,
|
||||
};
|
||||
mockedUseGeminiStream.mockReturnValue(geminiStreamMock);
|
||||
|
||||
await setupKeypressTest();
|
||||
|
||||
// Update the mock state when toggled to simulate real behavior
|
||||
mockToggleBackgroundTask.mockImplementation(() => {
|
||||
geminiStreamMock.isBackgroundTaskVisible = true;
|
||||
mockToggleBackgroundShell.mockImplementation(() => {
|
||||
geminiStreamMock.isBackgroundShellVisible = true;
|
||||
});
|
||||
|
||||
// Press Ctrl+B
|
||||
pressKey('\x02');
|
||||
|
||||
// Should have toggled (shown) the shell
|
||||
expect(mockToggleBackgroundTask).toHaveBeenCalled();
|
||||
expect(mockToggleBackgroundShell).toHaveBeenCalled();
|
||||
// Should be focused
|
||||
expect(capturedUIState.embeddedShellFocused).toBe(true);
|
||||
|
||||
|
||||
@@ -83,7 +83,6 @@ import {
|
||||
logBillingEvent,
|
||||
ApiKeyUpdatedEvent,
|
||||
type InjectionSource,
|
||||
startMemoryService,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from '../config/auth.js';
|
||||
import process from 'node:process';
|
||||
@@ -111,7 +110,7 @@ import { computeTerminalTitle } from '../utils/windowTitle.js';
|
||||
import { useTextBuffer } from './components/shared/text-buffer.js';
|
||||
import { useLogger } from './hooks/useLogger.js';
|
||||
import { useGeminiStream } from './hooks/useGeminiStream.js';
|
||||
import { type BackgroundTask } from './hooks/useExecutionLifecycle.js';
|
||||
import { type BackgroundShell } from './hooks/shellCommandProcessor.js';
|
||||
import { useVim } from './hooks/vim.js';
|
||||
import { type LoadableSettingScope, SettingScope } from '../config/settings.js';
|
||||
import { type InitializationResult } from '../core/initializer.js';
|
||||
@@ -152,7 +151,7 @@ import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
|
||||
import { useBanner } from './hooks/useBanner.js';
|
||||
import { useTerminalSetupPrompt } from './utils/terminalSetup.js';
|
||||
import { useHookDisplayState } from './hooks/useHookDisplayState.js';
|
||||
import { useBackgroundTaskManager } from './hooks/useBackgroundTaskManager.js';
|
||||
import { useBackgroundShellManager } from './hooks/useBackgroundShellManager.js';
|
||||
import {
|
||||
WARNING_PROMPT_DURATION_MS,
|
||||
QUEUE_ERROR_DISPLAY_DURATION_MS,
|
||||
@@ -169,7 +168,6 @@ import { useSuspend } from './hooks/useSuspend.js';
|
||||
import { useRunEventNotifications } from './hooks/useRunEventNotifications.js';
|
||||
import { isNotificationsEnabled } from '../utils/terminalNotifications.js';
|
||||
import {
|
||||
getLastTurnToolCallIds,
|
||||
isToolExecuting,
|
||||
isToolAwaitingConfirmation,
|
||||
getAllToolCalls,
|
||||
@@ -234,45 +232,12 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
);
|
||||
const [copyModeEnabled, setCopyModeEnabled] = useState(false);
|
||||
const [pendingRestorePrompt, setPendingRestorePrompt] = useState(false);
|
||||
const toggleBackgroundTasksRef = useRef<() => void>(() => {});
|
||||
const isBackgroundTaskVisibleRef = useRef<boolean>(false);
|
||||
const backgroundTasksRef = useRef<Map<number, BackgroundTask>>(new Map());
|
||||
const toggleBackgroundShellRef = useRef<() => void>(() => {});
|
||||
const isBackgroundShellVisibleRef = useRef<boolean>(false);
|
||||
const backgroundShellsRef = useRef<Map<number, BackgroundShell>>(new Map());
|
||||
|
||||
const [adminSettingsChanged, setAdminSettingsChanged] = useState(false);
|
||||
|
||||
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleExpansion = useCallback((callId: string) => {
|
||||
setExpandedTools((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(callId)) {
|
||||
next.delete(callId);
|
||||
} else {
|
||||
next.add(callId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleAllExpansion = useCallback((callIds: string[]) => {
|
||||
setExpandedTools((prev) => {
|
||||
const next = new Set(prev);
|
||||
const anyCollapsed = callIds.some((id) => !next.has(id));
|
||||
|
||||
if (anyCollapsed) {
|
||||
callIds.forEach((id) => next.add(id));
|
||||
} else {
|
||||
callIds.forEach((id) => next.delete(id));
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isExpanded = useCallback(
|
||||
(callId: string) => expandedTools.has(callId),
|
||||
[expandedTools],
|
||||
);
|
||||
|
||||
const [shellModeActive, setShellModeActive] = useState(false);
|
||||
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
|
||||
useState<boolean>(false);
|
||||
@@ -448,13 +413,6 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
setConfigInitialized(true);
|
||||
startupProfiler.flush(config);
|
||||
|
||||
// Fire-and-forget memory service (skill extraction from past sessions)
|
||||
if (config.isMemoryManagerEnabled()) {
|
||||
startMemoryService(config).catch((e) => {
|
||||
debugLogger.error('Failed to start memory service:', e);
|
||||
});
|
||||
}
|
||||
|
||||
const sessionStartSource = resumedSessionData
|
||||
? SessionStartSource.Resume
|
||||
: SessionStartSource.Startup;
|
||||
@@ -496,7 +454,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
|
||||
// Kill all background shells
|
||||
await Promise.all(
|
||||
Array.from(backgroundTasksRef.current.keys()).map((pid) =>
|
||||
Array.from(backgroundShellsRef.current.keys()).map((pid) =>
|
||||
ShellExecutionService.kill(pid),
|
||||
),
|
||||
);
|
||||
@@ -907,7 +865,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
const { toggleVimEnabled } = useVimMode();
|
||||
|
||||
const setIsBackgroundTaskListOpenRef = useRef<(open: boolean) => void>(
|
||||
const setIsBackgroundShellListOpenRef = useRef<(open: boolean) => void>(
|
||||
() => {},
|
||||
);
|
||||
const [shortcutsHelpVisible, setShortcutsHelpVisible] = useState(false);
|
||||
@@ -942,14 +900,14 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
toggleDebugProfiler,
|
||||
dispatchExtensionStateUpdate,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
toggleBackgroundTasks: () => {
|
||||
toggleBackgroundTasksRef.current();
|
||||
if (!isBackgroundTaskVisibleRef.current) {
|
||||
toggleBackgroundShell: () => {
|
||||
toggleBackgroundShellRef.current();
|
||||
if (!isBackgroundShellVisibleRef.current) {
|
||||
setEmbeddedShellFocused(true);
|
||||
if (backgroundTasksRef.current.size > 1) {
|
||||
setIsBackgroundTaskListOpenRef.current(true);
|
||||
if (backgroundShellsRef.current.size > 1) {
|
||||
setIsBackgroundShellListOpenRef.current(true);
|
||||
} else {
|
||||
setIsBackgroundTaskListOpenRef.current(false);
|
||||
setIsBackgroundShellListOpenRef.current(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1035,7 +993,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
if (config.isJitContextEnabled()) {
|
||||
await config.getContextManager()?.refresh();
|
||||
config.updateSystemInstructionIfInitialized();
|
||||
flattenedMemory = flattenMemory(config.getUserMemory());
|
||||
fileCount = config.getGeminiMdFileCount();
|
||||
} else {
|
||||
@@ -1122,7 +1079,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
useEffect(() => {
|
||||
const hintListener = (text: string, source: InjectionSource) => {
|
||||
if (source !== 'user_steering' && source !== 'background_completion') {
|
||||
if (source !== 'user_steering') {
|
||||
return;
|
||||
}
|
||||
pendingHintsRef.current.push(text);
|
||||
@@ -1146,12 +1103,12 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
lastOutputTime,
|
||||
backgroundTaskCount,
|
||||
isBackgroundTaskVisible,
|
||||
toggleBackgroundTasks,
|
||||
backgroundCurrentExecution,
|
||||
backgroundTasks,
|
||||
dismissBackgroundTask,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
toggleBackgroundShell,
|
||||
backgroundCurrentShell,
|
||||
backgroundShells,
|
||||
dismissBackgroundShell,
|
||||
retryStatus,
|
||||
} = useGeminiStream(
|
||||
config.getGeminiClient(),
|
||||
@@ -1180,27 +1137,32 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
|
||||
);
|
||||
|
||||
toggleBackgroundTasksRef.current = toggleBackgroundTasks;
|
||||
isBackgroundTaskVisibleRef.current = isBackgroundTaskVisible;
|
||||
backgroundTasksRef.current = backgroundTasks;
|
||||
const hasPendingToolConfirmation = useMemo(
|
||||
() => isToolAwaitingConfirmation(pendingHistoryItems),
|
||||
[pendingHistoryItems],
|
||||
);
|
||||
|
||||
toggleBackgroundShellRef.current = toggleBackgroundShell;
|
||||
isBackgroundShellVisibleRef.current = isBackgroundShellVisible;
|
||||
backgroundShellsRef.current = backgroundShells;
|
||||
|
||||
const {
|
||||
activeBackgroundTaskPid,
|
||||
setIsBackgroundTaskListOpen,
|
||||
isBackgroundTaskListOpen,
|
||||
setActiveBackgroundTaskPid,
|
||||
backgroundTaskHeight,
|
||||
} = useBackgroundTaskManager({
|
||||
backgroundTasks,
|
||||
backgroundTaskCount,
|
||||
isBackgroundTaskVisible,
|
||||
activeBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
isBackgroundShellListOpen,
|
||||
setActiveBackgroundShellPid,
|
||||
backgroundShellHeight,
|
||||
} = useBackgroundShellManager({
|
||||
backgroundShells,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
setEmbeddedShellFocused,
|
||||
terminalHeight,
|
||||
});
|
||||
|
||||
setIsBackgroundTaskListOpenRef.current = setIsBackgroundTaskListOpen;
|
||||
setIsBackgroundShellListOpenRef.current = setIsBackgroundShellListOpen;
|
||||
|
||||
const lastOutputTimeRef = useRef(0);
|
||||
|
||||
@@ -1472,7 +1434,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
// Compute available terminal height based on stable controls measurement
|
||||
const availableTerminalHeight = Math.max(
|
||||
0,
|
||||
terminalHeight - stableControlsHeight - backgroundTaskHeight - 1,
|
||||
terminalHeight - stableControlsHeight - backgroundShellHeight - 1,
|
||||
);
|
||||
|
||||
config.setShellExecutionConfig({
|
||||
@@ -1765,25 +1727,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
return true;
|
||||
}
|
||||
|
||||
const toggleLastTurnTools = () => {
|
||||
triggerExpandHint(true);
|
||||
|
||||
const targetToolCallIds = getLastTurnToolCallIds(
|
||||
historyManager.history,
|
||||
pendingHistoryItems,
|
||||
);
|
||||
|
||||
if (targetToolCallIds.length > 0) {
|
||||
toggleAllExpansion(targetToolCallIds);
|
||||
}
|
||||
};
|
||||
|
||||
let enteringConstrainHeightMode = false;
|
||||
if (!constrainHeight) {
|
||||
enteringConstrainHeightMode = true;
|
||||
setConstrainHeight(true);
|
||||
if (keyMatchers[Command.SHOW_MORE_LINES](key)) {
|
||||
toggleLastTurnTools();
|
||||
// If the user manually collapses the view, show the hint and reset the x-second timer.
|
||||
triggerExpandHint(true);
|
||||
}
|
||||
if (!isAlternateBuffer) {
|
||||
refreshStatic();
|
||||
@@ -1831,13 +1781,16 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
!enteringConstrainHeightMode
|
||||
) {
|
||||
setConstrainHeight(false);
|
||||
toggleLastTurnTools();
|
||||
refreshStatic();
|
||||
// If the user manually expands the view, show the hint and reset the x-second timer.
|
||||
triggerExpandHint(true);
|
||||
if (!isAlternateBuffer) {
|
||||
refreshStatic();
|
||||
}
|
||||
return true;
|
||||
} else if (
|
||||
(keyMatchers[Command.FOCUS_SHELL_INPUT](key) ||
|
||||
keyMatchers[Command.UNFOCUS_BACKGROUND_SHELL_LIST](key)) &&
|
||||
(activePtyId || (isBackgroundTaskVisible && backgroundTasks.size > 0))
|
||||
(activePtyId || (isBackgroundShellVisible && backgroundShells.size > 0))
|
||||
) {
|
||||
if (embeddedShellFocused) {
|
||||
const capturedTime = lastOutputTimeRef.current;
|
||||
@@ -1858,12 +1811,12 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
const isIdle = Date.now() - lastOutputTimeRef.current >= 100;
|
||||
|
||||
if (isIdle && !activePtyId && !isBackgroundTaskVisible) {
|
||||
if (isIdle && !activePtyId && !isBackgroundShellVisible) {
|
||||
if (tabFocusTimeoutRef.current)
|
||||
clearTimeout(tabFocusTimeoutRef.current);
|
||||
toggleBackgroundTasks();
|
||||
toggleBackgroundShell();
|
||||
setEmbeddedShellFocused(true);
|
||||
if (backgroundTasks.size > 1) setIsBackgroundTaskListOpen(true);
|
||||
if (backgroundShells.size > 1) setIsBackgroundShellListOpen(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1880,15 +1833,15 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
return false;
|
||||
} else if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL](key)) {
|
||||
if (activePtyId) {
|
||||
backgroundCurrentExecution();
|
||||
backgroundCurrentShell();
|
||||
// After backgrounding, we explicitly do NOT show or focus the background UI.
|
||||
} else {
|
||||
toggleBackgroundTasks();
|
||||
toggleBackgroundShell();
|
||||
// Toggle focus based on intent: if we were hiding, unfocus; if showing, focus.
|
||||
if (!isBackgroundTaskVisible && backgroundTasks.size > 0) {
|
||||
if (!isBackgroundShellVisible && backgroundShells.size > 0) {
|
||||
setEmbeddedShellFocused(true);
|
||||
if (backgroundTasks.size > 1) {
|
||||
setIsBackgroundTaskListOpen(true);
|
||||
if (backgroundShells.size > 1) {
|
||||
setIsBackgroundShellListOpen(true);
|
||||
}
|
||||
} else {
|
||||
setEmbeddedShellFocused(false);
|
||||
@@ -1896,11 +1849,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}
|
||||
return true;
|
||||
} else if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL_LIST](key)) {
|
||||
if (backgroundTasks.size > 0 && isBackgroundTaskVisible) {
|
||||
if (backgroundShells.size > 0 && isBackgroundShellVisible) {
|
||||
if (!embeddedShellFocused) {
|
||||
setEmbeddedShellFocused(true);
|
||||
}
|
||||
setIsBackgroundTaskListOpen(true);
|
||||
setIsBackgroundShellListOpen(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1925,11 +1878,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
tabFocusTimeoutRef,
|
||||
isAlternateBuffer,
|
||||
shortcutsHelpVisible,
|
||||
backgroundCurrentExecution,
|
||||
toggleBackgroundTasks,
|
||||
backgroundTasks,
|
||||
isBackgroundTaskVisible,
|
||||
setIsBackgroundTaskListOpen,
|
||||
backgroundCurrentShell,
|
||||
toggleBackgroundShell,
|
||||
backgroundShells,
|
||||
isBackgroundShellVisible,
|
||||
setIsBackgroundShellListOpen,
|
||||
lastOutputTimeRef,
|
||||
showTransientMessage,
|
||||
settings.merged.general.devtools,
|
||||
@@ -1937,9 +1890,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
triggerExpandHint,
|
||||
keyMatchers,
|
||||
isHelpDismissKey,
|
||||
historyManager.history,
|
||||
pendingHistoryItems,
|
||||
toggleAllExpansion,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2083,11 +2033,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
authState === AuthState.AwaitingApiKeyInput ||
|
||||
!!newAgents;
|
||||
|
||||
const hasPendingToolConfirmation = useMemo(
|
||||
() => isToolAwaitingConfirmation(pendingHistoryItems),
|
||||
[pendingHistoryItems],
|
||||
);
|
||||
|
||||
const hasConfirmUpdateExtensionRequests =
|
||||
confirmUpdateExtensionRequests.length > 0;
|
||||
const hasLoopDetectionConfirmationRequest =
|
||||
@@ -2110,7 +2055,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
const showStatusWit = loadingPhrases === 'witty' || loadingPhrases === 'all';
|
||||
|
||||
const showLoadingIndicator =
|
||||
(!embeddedShellFocused || isBackgroundTaskVisible) &&
|
||||
(!embeddedShellFocused || isBackgroundShellVisible) &&
|
||||
streamingState === StreamingState.Responding &&
|
||||
!hasPendingActionRequired;
|
||||
|
||||
@@ -2368,8 +2313,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isRestarting,
|
||||
extensionsUpdateState,
|
||||
activePtyId,
|
||||
backgroundTaskCount,
|
||||
isBackgroundTaskVisible,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
embeddedShellFocused,
|
||||
showDebugProfiler,
|
||||
customDialog,
|
||||
@@ -2379,10 +2324,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
bannerVisible,
|
||||
terminalBackgroundColor: config.getTerminalBackground(),
|
||||
settingsNonce,
|
||||
backgroundTasks,
|
||||
activeBackgroundTaskPid,
|
||||
backgroundTaskHeight,
|
||||
isBackgroundTaskListOpen,
|
||||
backgroundShells,
|
||||
activeBackgroundShellPid,
|
||||
backgroundShellHeight,
|
||||
isBackgroundShellListOpen,
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
showIsExpandableHint,
|
||||
@@ -2491,8 +2436,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
currentModel,
|
||||
extensionsUpdateState,
|
||||
activePtyId,
|
||||
backgroundTaskCount,
|
||||
isBackgroundTaskVisible,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
historyManager,
|
||||
embeddedShellFocused,
|
||||
showDebugProfiler,
|
||||
@@ -2505,10 +2450,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
bannerVisible,
|
||||
config,
|
||||
settingsNonce,
|
||||
backgroundTaskHeight,
|
||||
isBackgroundTaskListOpen,
|
||||
activeBackgroundTaskPid,
|
||||
backgroundTasks,
|
||||
backgroundShellHeight,
|
||||
isBackgroundShellListOpen,
|
||||
activeBackgroundShellPid,
|
||||
backgroundShells,
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
showIsExpandableHint,
|
||||
@@ -2568,9 +2513,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
revealCleanUiDetailsTemporarily,
|
||||
handleWarning,
|
||||
setEmbeddedShellFocused,
|
||||
dismissBackgroundTask,
|
||||
setActiveBackgroundTaskPid,
|
||||
setIsBackgroundTaskListOpen,
|
||||
dismissBackgroundShell,
|
||||
setActiveBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
setAuthContext,
|
||||
onHintInput: () => {},
|
||||
onHintBackspace: () => {},
|
||||
@@ -2660,9 +2605,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
revealCleanUiDetailsTemporarily,
|
||||
handleWarning,
|
||||
setEmbeddedShellFocused,
|
||||
dismissBackgroundTask,
|
||||
setActiveBackgroundTaskPid,
|
||||
setIsBackgroundTaskListOpen,
|
||||
dismissBackgroundShell,
|
||||
setActiveBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
setAuthContext,
|
||||
setAccountSuspensionInfo,
|
||||
newAgents,
|
||||
@@ -2694,13 +2639,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
startupWarnings: props.startupWarnings || [],
|
||||
}}
|
||||
>
|
||||
<ToolActionsProvider
|
||||
config={config}
|
||||
toolCalls={allToolCalls}
|
||||
isExpanded={isExpanded}
|
||||
toggleExpansion={toggleExpansion}
|
||||
toggleAllExpansion={toggleAllExpansion}
|
||||
>
|
||||
<ToolActionsProvider config={config} toolCalls={allToolCalls}>
|
||||
<ShellFocusContext.Provider value={isFocused}>
|
||||
<App key={`app-${forceRerenderKey}`} />
|
||||
</ShellFocusContext.Provider>
|
||||
|
||||
@@ -65,7 +65,7 @@ const getSavedChatTags = async (
|
||||
);
|
||||
|
||||
return chatDetails;
|
||||
} catch {
|
||||
} catch (_err) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -75,7 +75,6 @@ const listCommand: SlashCommand = {
|
||||
description: 'List saved manual conversation checkpoints',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
takesArgs: false,
|
||||
action: async (context): Promise<void> => {
|
||||
const chatDetails = await getSavedChatTags(context, false);
|
||||
|
||||
@@ -407,24 +406,14 @@ export const chatResumeSubCommands: SlashCommand[] = [
|
||||
checkpointCompatibilityCommand,
|
||||
];
|
||||
|
||||
import { parseSlashCommand } from '../../utils/commands.js';
|
||||
|
||||
export const chatCommand: SlashCommand = {
|
||||
name: 'chat',
|
||||
description: 'Browse auto-saved conversations and manage chat checkpoints',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context, args) => {
|
||||
if (args) {
|
||||
const parsed = parseSlashCommand(`/${args}`, chatResumeSubCommands);
|
||||
if (parsed.commandToExecute?.action) {
|
||||
return parsed.commandToExecute.action(context, parsed.args);
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'dialog',
|
||||
dialog: 'sessionBrowser',
|
||||
};
|
||||
},
|
||||
action: async () => ({
|
||||
type: 'dialog',
|
||||
dialog: 'sessionBrowser',
|
||||
}),
|
||||
subCommands: chatResumeSubCommands,
|
||||
};
|
||||
|
||||
@@ -198,7 +198,7 @@ export const directoryCommand: SlashCommand = {
|
||||
alreadyAdded.push(trimmedPath);
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
// Path might not exist or be inaccessible.
|
||||
// We'll let batchAddDirectories handle it later.
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ async function exploreAction(
|
||||
});
|
||||
try {
|
||||
await open(extensionsUrl);
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
text: `Failed to open browser. Check out the extensions gallery at ${extensionsUrl}`,
|
||||
@@ -789,7 +789,6 @@ const listExtensionsCommand: SlashCommand = {
|
||||
description: 'List active extensions',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
takesArgs: false,
|
||||
action: listAction,
|
||||
};
|
||||
|
||||
@@ -850,7 +849,6 @@ const exploreExtensionsCommand: SlashCommand = {
|
||||
description: 'Open extensions page in your browser',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
takesArgs: false,
|
||||
action: exploreAction,
|
||||
};
|
||||
|
||||
@@ -872,8 +870,6 @@ const configCommand: SlashCommand = {
|
||||
action: configAction,
|
||||
};
|
||||
|
||||
import { parseSlashCommand } from '../../utils/commands.js';
|
||||
|
||||
export function extensionsCommand(
|
||||
enableExtensionReloading?: boolean,
|
||||
): SlashCommand {
|
||||
@@ -887,29 +883,20 @@ export function extensionsCommand(
|
||||
configCommand,
|
||||
]
|
||||
: [];
|
||||
const subCommands = [
|
||||
listExtensionsCommand,
|
||||
updateExtensionsCommand,
|
||||
exploreExtensionsCommand,
|
||||
reloadCommand,
|
||||
...conditionalCommands,
|
||||
];
|
||||
|
||||
return {
|
||||
name: 'extensions',
|
||||
description: 'Manage extensions',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands,
|
||||
action: async (context, args) => {
|
||||
if (args) {
|
||||
const parsed = parseSlashCommand(`/${args}`, subCommands);
|
||||
if (parsed.commandToExecute?.action) {
|
||||
return parsed.commandToExecute.action(context, parsed.args);
|
||||
}
|
||||
}
|
||||
subCommands: [
|
||||
listExtensionsCommand,
|
||||
updateExtensionsCommand,
|
||||
exploreExtensionsCommand,
|
||||
reloadCommand,
|
||||
...conditionalCommands,
|
||||
],
|
||||
action: (context, args) =>
|
||||
// Default to list if no subcommand is provided
|
||||
return listExtensionsCommand.action!(context, args);
|
||||
},
|
||||
listExtensionsCommand.action!(context, args),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import type { CallableTool } from '@google/genai';
|
||||
import { MessageType, type HistoryItemMcpStatus } from '../types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
@@ -280,41 +280,5 @@ describe('mcpCommand', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter servers by name when an argument is provided to list', async () => {
|
||||
await mcpCommand.action!(mockContext, 'list server1');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.MCP_STATUS,
|
||||
servers: expect.objectContaining({
|
||||
server1: expect.any(Object),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// Should NOT contain server2 or server3
|
||||
const call = vi.mocked(mockContext.ui.addItem).mock
|
||||
.calls[0][0] as HistoryItemMcpStatus;
|
||||
expect(Object.keys(call.servers)).toEqual(['server1']);
|
||||
});
|
||||
|
||||
it('should filter servers by name and show descriptions when an argument is provided to desc', async () => {
|
||||
await mcpCommand.action!(mockContext, 'desc server2');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.MCP_STATUS,
|
||||
showDescriptions: true,
|
||||
servers: expect.objectContaining({
|
||||
server2: expect.any(Object),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const call = vi.mocked(mockContext.ui.addItem).mock
|
||||
.calls[0][0] as HistoryItemMcpStatus;
|
||||
expect(Object.keys(call.servers)).toEqual(['server2']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
canLoadServer,
|
||||
} from '../../config/mcp/mcpServerEnablement.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { parseSlashCommand } from '../../utils/commands.js';
|
||||
|
||||
const authCommand: SlashCommand = {
|
||||
name: 'auth',
|
||||
@@ -178,7 +177,6 @@ const listAction = async (
|
||||
context: CommandContext,
|
||||
showDescriptions = false,
|
||||
showSchema = false,
|
||||
serverNameFilter?: string,
|
||||
): Promise<void | MessageActionReturn> => {
|
||||
const agentContext = context.services.agentContext;
|
||||
const config = agentContext?.config;
|
||||
@@ -201,25 +199,11 @@ const listAction = async (
|
||||
};
|
||||
}
|
||||
|
||||
let mcpServers = config.getMcpClientManager()?.getMcpServers() || {};
|
||||
const mcpServers = config.getMcpClientManager()?.getMcpServers() || {};
|
||||
const serverNames = Object.keys(mcpServers);
|
||||
const blockedMcpServers =
|
||||
config.getMcpClientManager()?.getBlockedMcpServers() || [];
|
||||
|
||||
if (serverNameFilter) {
|
||||
const filter = serverNameFilter.trim().toLowerCase();
|
||||
if (filter) {
|
||||
mcpServers = Object.fromEntries(
|
||||
Object.entries(mcpServers).filter(
|
||||
([name]) =>
|
||||
name.toLowerCase().includes(filter) ||
|
||||
normalizeServerId(name).includes(filter),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const serverNames = Object.keys(mcpServers);
|
||||
|
||||
const connectingServers = serverNames.filter(
|
||||
(name) => getMCPServerStatus(name) === MCPServerStatus.CONNECTING,
|
||||
);
|
||||
@@ -322,7 +306,7 @@ const listCommand: SlashCommand = {
|
||||
description: 'List configured MCP servers and tools',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context, args) => listAction(context, false, false, args),
|
||||
action: (context) => listAction(context),
|
||||
};
|
||||
|
||||
const descCommand: SlashCommand = {
|
||||
@@ -331,7 +315,7 @@ const descCommand: SlashCommand = {
|
||||
description: 'List configured MCP servers and tools with descriptions',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context, args) => listAction(context, true, false, args),
|
||||
action: (context) => listAction(context, true),
|
||||
};
|
||||
|
||||
const schemaCommand: SlashCommand = {
|
||||
@@ -340,7 +324,7 @@ const schemaCommand: SlashCommand = {
|
||||
'List configured MCP servers and tools with descriptions and schemas',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context, args) => listAction(context, true, true, args),
|
||||
action: (context) => listAction(context, true, true),
|
||||
};
|
||||
|
||||
const reloadCommand: SlashCommand = {
|
||||
@@ -349,7 +333,6 @@ const reloadCommand: SlashCommand = {
|
||||
description: 'Reloads MCP servers',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
takesArgs: false,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
): Promise<void | SlashCommandActionReturn> => {
|
||||
@@ -547,18 +530,5 @@ export const mcpCommand: SlashCommand = {
|
||||
enableCommand,
|
||||
disableCommand,
|
||||
],
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
): Promise<void | SlashCommandActionReturn> => {
|
||||
if (args) {
|
||||
const parsed = parseSlashCommand(`/${args}`, mcpCommand.subCommands!);
|
||||
if (parsed.commandToExecute?.action) {
|
||||
return parsed.commandToExecute.action(context, parsed.args);
|
||||
}
|
||||
// If no subcommand matches, treat the whole args as a filter for list
|
||||
return listAction(context, false, false, args);
|
||||
}
|
||||
return listAction(context);
|
||||
},
|
||||
action: async (context: CommandContext) => listAction(context),
|
||||
};
|
||||
|
||||
@@ -104,47 +104,6 @@ describe('planCommand', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not return a submit_prompt action if arguments are empty', async () => {
|
||||
vi.mocked(
|
||||
mockContext.services.agentContext!.config.isPlanEnabled,
|
||||
).mockReturnValue(true);
|
||||
mockContext.invocation = {
|
||||
raw: '/plan',
|
||||
name: 'plan',
|
||||
args: '',
|
||||
};
|
||||
|
||||
if (!planCommand.action) throw new Error('Action missing');
|
||||
const result = await planCommand.action(mockContext, '');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(
|
||||
mockContext.services.agentContext!.config.setApprovalMode,
|
||||
).toHaveBeenCalledWith(ApprovalMode.PLAN);
|
||||
});
|
||||
|
||||
it('should return a submit_prompt action if arguments are provided', async () => {
|
||||
vi.mocked(
|
||||
mockContext.services.agentContext!.config.isPlanEnabled,
|
||||
).mockReturnValue(true);
|
||||
mockContext.invocation = {
|
||||
raw: '/plan implement auth',
|
||||
name: 'plan',
|
||||
args: 'implement auth',
|
||||
};
|
||||
|
||||
if (!planCommand.action) throw new Error('Action missing');
|
||||
const result = await planCommand.action(mockContext, 'implement auth');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'submit_prompt',
|
||||
content: 'implement auth',
|
||||
});
|
||||
expect(
|
||||
mockContext.services.agentContext!.config.setApprovalMode,
|
||||
).toHaveBeenCalledWith(ApprovalMode.PLAN);
|
||||
});
|
||||
|
||||
it('should display the approved plan from config', async () => {
|
||||
const mockPlanPath = '/mock/plans/dir/approved-plan.md';
|
||||
vi.mocked(
|
||||
|
||||
@@ -66,13 +66,6 @@ export const planCommand: SlashCommand = {
|
||||
coreEvents.emitFeedback('info', 'Switched to Plan Mode.');
|
||||
}
|
||||
|
||||
if (context.invocation?.args) {
|
||||
return {
|
||||
type: 'submit_prompt',
|
||||
content: context.invocation.args,
|
||||
};
|
||||
}
|
||||
|
||||
const approvedPlanPath = config.getApprovedPlanPath();
|
||||
|
||||
if (!approvedPlanPath) {
|
||||
@@ -93,14 +86,12 @@ export const planCommand: SlashCommand = {
|
||||
type: MessageType.GEMINI,
|
||||
text: partToString(content.llmContent),
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Failed to read approved plan at ${approvedPlanPath}: ${error}`,
|
||||
error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
subCommands: [
|
||||
@@ -109,7 +100,6 @@ export const planCommand: SlashCommand = {
|
||||
description: 'Copy the currently approved plan to your clipboard',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
takesArgs: false,
|
||||
action: copyAction,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -151,7 +151,7 @@ async function completion(
|
||||
const files = await fs.readdir(checkpointDir);
|
||||
const jsonFiles = files.filter((file) => file.endsWith('.json'));
|
||||
return getTruncatedCheckpointNames(jsonFiles);
|
||||
} catch {
|
||||
} catch (_err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export async function updateGitignore(gitRepoRoot: string): Promise<void> {
|
||||
let fileExists = true;
|
||||
try {
|
||||
existingContent = await fs.promises.readFile(gitignorePath, 'utf8');
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
// File doesn't exist
|
||||
fileExists = false;
|
||||
}
|
||||
@@ -168,8 +168,8 @@ async function downloadFiles({
|
||||
async function createDirectory(dirPath: string): Promise<void> {
|
||||
try {
|
||||
await fs.promises.mkdir(dirPath, { recursive: true });
|
||||
} catch (error) {
|
||||
debugLogger.debug(`Failed to create ${dirPath} directory:`, error);
|
||||
} catch (_error) {
|
||||
debugLogger.debug(`Failed to create ${dirPath} directory:`, _error);
|
||||
throw new Error(
|
||||
`Unable to create ${dirPath} directory. Do you have file permissions in the current directory?`,
|
||||
);
|
||||
@@ -222,8 +222,8 @@ export const setupGithubCommand: SlashCommand = {
|
||||
let gitRepoRoot: string;
|
||||
try {
|
||||
gitRepoRoot = getGitRepoRoot();
|
||||
} catch (error) {
|
||||
debugLogger.debug(`Failed to get git repo root:`, error);
|
||||
} catch (_error) {
|
||||
debugLogger.debug(`Failed to get git repo root:`, _error);
|
||||
throw new Error(
|
||||
'Unable to determine the GitHub repository. /setup-github must be run from a git repository.',
|
||||
);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { shellsCommand } from './shellsCommand.js';
|
||||
import type { CommandContext } from './types.js';
|
||||
|
||||
describe('shellsCommand', () => {
|
||||
it('should call toggleBackgroundShell', async () => {
|
||||
const toggleBackgroundShell = vi.fn();
|
||||
const context = {
|
||||
ui: {
|
||||
toggleBackgroundShell,
|
||||
},
|
||||
} as unknown as CommandContext;
|
||||
|
||||
if (shellsCommand.action) {
|
||||
await shellsCommand.action(context, '');
|
||||
}
|
||||
|
||||
expect(toggleBackgroundShell).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have correct name and altNames', () => {
|
||||
expect(shellsCommand.name).toBe('shells');
|
||||
expect(shellsCommand.altNames).toContain('bashes');
|
||||
});
|
||||
|
||||
it('should auto-execute', () => {
|
||||
expect(shellsCommand.autoExecute).toBe(true);
|
||||
});
|
||||
});
|
||||
+5
-5
@@ -6,13 +6,13 @@
|
||||
|
||||
import { CommandKind, type SlashCommand } from './types.js';
|
||||
|
||||
export const tasksCommand: SlashCommand = {
|
||||
name: 'tasks',
|
||||
altNames: ['bg', 'background'],
|
||||
export const shellsCommand: SlashCommand = {
|
||||
name: 'shells',
|
||||
altNames: ['bashes'],
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'Toggle background tasks view',
|
||||
description: 'Toggle background shells view',
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
context.ui.toggleBackgroundTasks();
|
||||
context.ui.toggleBackgroundShell();
|
||||
},
|
||||
};
|
||||
@@ -528,7 +528,6 @@ describe('skillsCommand', () => {
|
||||
await actionPromise;
|
||||
|
||||
expect(reloadSkillsMock).toHaveBeenCalled();
|
||||
expect(context.ui.reloadCommands).toHaveBeenCalled();
|
||||
expect(context.ui.setPendingItem).toHaveBeenCalledWith(null);
|
||||
expect(context.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -285,8 +285,6 @@ async function reloadAction(
|
||||
context.ui.setPendingItem(null);
|
||||
}
|
||||
|
||||
context.ui.reloadCommands();
|
||||
|
||||
const afterSkills = skillManager.getSkills();
|
||||
const afterNames = new Set(afterSkills.map((s) => s.name));
|
||||
|
||||
@@ -359,8 +357,6 @@ function enableCompletion(
|
||||
.map((s) => s.name);
|
||||
}
|
||||
|
||||
import { parseSlashCommand } from '../../utils/commands.js';
|
||||
|
||||
export const skillsCommand: SlashCommand = {
|
||||
name: 'skills',
|
||||
description:
|
||||
@@ -406,13 +402,5 @@ export const skillsCommand: SlashCommand = {
|
||||
action: reloadAction,
|
||||
},
|
||||
],
|
||||
action: async (context, args) => {
|
||||
if (args) {
|
||||
const parsed = parseSlashCommand(`/${args}`, skillsCommand.subCommands!);
|
||||
if (parsed.commandToExecute?.action) {
|
||||
return parsed.commandToExecute.action(context, parsed.args);
|
||||
}
|
||||
}
|
||||
return listAction(context, args);
|
||||
},
|
||||
action: listAction,
|
||||
};
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { tasksCommand } from './tasksCommand.js';
|
||||
import type { CommandContext } from './types.js';
|
||||
|
||||
describe('tasksCommand', () => {
|
||||
it('should call toggleBackgroundTasks', async () => {
|
||||
const toggleBackgroundTasks = vi.fn();
|
||||
const context = {
|
||||
ui: {
|
||||
toggleBackgroundTasks,
|
||||
},
|
||||
} as unknown as CommandContext;
|
||||
|
||||
if (tasksCommand.action) {
|
||||
await tasksCommand.action(context, '');
|
||||
}
|
||||
|
||||
expect(toggleBackgroundTasks).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have correct name and altNames', () => {
|
||||
expect(tasksCommand.name).toBe('tasks');
|
||||
expect(tasksCommand.altNames).toContain('bg');
|
||||
expect(tasksCommand.altNames).toContain('background');
|
||||
});
|
||||
|
||||
it('should auto-execute', () => {
|
||||
expect(tasksCommand.autoExecute).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -90,7 +90,7 @@ export interface CommandContext {
|
||||
*/
|
||||
setConfirmationRequest: (value: ConfirmationRequest) => void;
|
||||
removeComponent: () => void;
|
||||
toggleBackgroundTasks: () => void;
|
||||
toggleBackgroundShell: () => void;
|
||||
toggleShortcutsHelp: () => void;
|
||||
};
|
||||
// Session-specific data
|
||||
@@ -240,14 +240,5 @@ export interface SlashCommand {
|
||||
*/
|
||||
showCompletionLoading?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the command expects arguments.
|
||||
* If false, and the command is a subcommand, the command parser may treat
|
||||
* any following text as arguments for the parent command instead of this subcommand,
|
||||
* provided the parent command has an action.
|
||||
* Defaults to true.
|
||||
*/
|
||||
takesArgs?: boolean;
|
||||
|
||||
subCommands?: SlashCommand[];
|
||||
}
|
||||
|
||||
+32
-32
@@ -6,8 +6,8 @@
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { BackgroundTaskDisplay } from './BackgroundTaskDisplay.js';
|
||||
import { type BackgroundTask } from '../hooks/useExecutionLifecycle.js';
|
||||
import { BackgroundShellDisplay } from './BackgroundShellDisplay.js';
|
||||
import { type BackgroundShell } from '../hooks/shellCommandProcessor.js';
|
||||
import { ShellExecutionService } from '@google/gemini-cli-core';
|
||||
import { act } from 'react';
|
||||
import { type Key, type KeypressHandler } from '../contexts/KeypressContext.js';
|
||||
@@ -15,15 +15,15 @@ import { ScrollProvider } from '../contexts/ScrollProvider.js';
|
||||
import { Box } from 'ink';
|
||||
|
||||
// Mock dependencies
|
||||
const mockDismissBackgroundTask = vi.fn();
|
||||
const mockSetActiveBackgroundTaskPid = vi.fn();
|
||||
const mockSetIsBackgroundTaskListOpen = vi.fn();
|
||||
const mockDismissBackgroundShell = vi.fn();
|
||||
const mockSetActiveBackgroundShellPid = vi.fn();
|
||||
const mockSetIsBackgroundShellListOpen = vi.fn();
|
||||
|
||||
vi.mock('../contexts/UIActionsContext.js', () => ({
|
||||
useUIActions: () => ({
|
||||
dismissBackgroundTask: mockDismissBackgroundTask,
|
||||
setActiveBackgroundTaskPid: mockSetActiveBackgroundTaskPid,
|
||||
setIsBackgroundTaskListOpen: mockSetIsBackgroundTaskListOpen,
|
||||
dismissBackgroundShell: mockDismissBackgroundShell,
|
||||
setActiveBackgroundShellPid: mockSetActiveBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen: mockSetIsBackgroundShellListOpen,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -86,14 +86,14 @@ vi.mock('./shared/ScrollableList.js', () => ({
|
||||
data,
|
||||
renderItem,
|
||||
}: {
|
||||
data: BackgroundTask[];
|
||||
data: BackgroundShell[];
|
||||
renderItem: (props: {
|
||||
item: BackgroundTask;
|
||||
item: BackgroundShell;
|
||||
index: number;
|
||||
}) => React.ReactNode;
|
||||
}) => (
|
||||
<Box flexDirection="column">
|
||||
{data.map((item: BackgroundTask, index: number) => (
|
||||
{data.map((item: BackgroundShell, index: number) => (
|
||||
<Box key={index}>{renderItem({ item, index })}</Box>
|
||||
))}
|
||||
</Box>
|
||||
@@ -116,9 +116,9 @@ const createMockKey = (overrides: Partial<Key>): Key => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('<BackgroundTaskDisplay />', () => {
|
||||
const mockShells = new Map<number, BackgroundTask>();
|
||||
const shell1: BackgroundTask = {
|
||||
describe('<BackgroundShellDisplay />', () => {
|
||||
const mockShells = new Map<number, BackgroundShell>();
|
||||
const shell1: BackgroundShell = {
|
||||
pid: 1001,
|
||||
command: 'npm start',
|
||||
output: 'Starting server...',
|
||||
@@ -126,7 +126,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
binaryBytesReceived: 0,
|
||||
status: 'running',
|
||||
};
|
||||
const shell2: BackgroundTask = {
|
||||
const shell2: BackgroundShell = {
|
||||
pid: 1002,
|
||||
command: 'tail -f log.txt',
|
||||
output: 'Log entry 1',
|
||||
@@ -147,7 +147,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
const width = 80;
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundTaskDisplay
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={width}
|
||||
@@ -167,7 +167,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
const width = 100;
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundTaskDisplay
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={width}
|
||||
@@ -187,7 +187,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
const width = 80;
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundTaskDisplay
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={width}
|
||||
@@ -207,7 +207,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
const width = 80;
|
||||
const { rerender, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundTaskDisplay
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={width}
|
||||
@@ -227,7 +227,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
|
||||
rerender(
|
||||
<ScrollProvider>
|
||||
<BackgroundTaskDisplay
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={100}
|
||||
@@ -250,7 +250,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
const width = 80;
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundTaskDisplay
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={width}
|
||||
@@ -270,7 +270,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
const width = 80;
|
||||
const { unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundTaskDisplay
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={width}
|
||||
@@ -287,13 +287,13 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
simulateKey({ name: 'down' });
|
||||
});
|
||||
|
||||
// Simulate Ctrl+L (handled by BackgroundTaskDisplay)
|
||||
// Simulate Ctrl+L (handled by BackgroundShellDisplay)
|
||||
await act(async () => {
|
||||
simulateKey({ name: 'l', ctrl: true });
|
||||
});
|
||||
|
||||
expect(mockSetActiveBackgroundTaskPid).toHaveBeenCalledWith(shell2.pid);
|
||||
expect(mockSetIsBackgroundTaskListOpen).toHaveBeenCalledWith(false);
|
||||
expect(mockSetActiveBackgroundShellPid).toHaveBeenCalledWith(shell2.pid);
|
||||
expect(mockSetIsBackgroundShellListOpen).toHaveBeenCalledWith(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -301,7 +301,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
const width = 80;
|
||||
const { unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundTaskDisplay
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={width}
|
||||
@@ -325,7 +325,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
simulateKey({ name: 'k', ctrl: true });
|
||||
});
|
||||
|
||||
expect(mockDismissBackgroundTask).toHaveBeenCalledWith(shell2.pid);
|
||||
expect(mockDismissBackgroundShell).toHaveBeenCalledWith(shell2.pid);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -333,7 +333,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
const width = 80;
|
||||
const { unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundTaskDisplay
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={width}
|
||||
@@ -349,7 +349,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
simulateKey({ name: 'k', ctrl: true });
|
||||
});
|
||||
|
||||
expect(mockDismissBackgroundTask).toHaveBeenCalledWith(shell1.pid);
|
||||
expect(mockDismissBackgroundShell).toHaveBeenCalledWith(shell1.pid);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -358,7 +358,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
const width = 80;
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundTaskDisplay
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell2.pid}
|
||||
width={width}
|
||||
@@ -375,7 +375,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
});
|
||||
|
||||
it('keeps exit code status color even when selected', async () => {
|
||||
const exitedShell: BackgroundTask = {
|
||||
const exitedShell: BackgroundShell = {
|
||||
pid: 1003,
|
||||
command: 'exit 0',
|
||||
output: '',
|
||||
@@ -389,7 +389,7 @@ describe('<BackgroundTaskDisplay />', () => {
|
||||
const width = 80;
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundTaskDisplay
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={exitedShell.pid}
|
||||
width={width}
|
||||
+16
-16
@@ -17,7 +17,7 @@ import {
|
||||
type AnsiToken,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { cpLen, cpSlice, getCachedStringWidth } from '../utils/textUtils.js';
|
||||
import { type BackgroundTask } from '../hooks/useExecutionLifecycle.js';
|
||||
import { type BackgroundShell } from '../hooks/shellCommandProcessor.js';
|
||||
import { Command } from '../key/keyMatchers.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { formatCommand } from '../key/keybindingUtils.js';
|
||||
@@ -34,8 +34,8 @@ import {
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
interface BackgroundTaskDisplayProps {
|
||||
shells: Map<number, BackgroundTask>;
|
||||
interface BackgroundShellDisplayProps {
|
||||
shells: Map<number, BackgroundShell>;
|
||||
activePid: number;
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -61,19 +61,19 @@ const formatShellCommandForDisplay = (command: string, maxWidth: number) => {
|
||||
: commandFirstLine;
|
||||
};
|
||||
|
||||
export const BackgroundTaskDisplay = ({
|
||||
export const BackgroundShellDisplay = ({
|
||||
shells,
|
||||
activePid,
|
||||
width,
|
||||
height,
|
||||
isFocused,
|
||||
isListOpenProp,
|
||||
}: BackgroundTaskDisplayProps) => {
|
||||
}: BackgroundShellDisplayProps) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const {
|
||||
dismissBackgroundTask,
|
||||
setActiveBackgroundTaskPid,
|
||||
setIsBackgroundTaskListOpen,
|
||||
dismissBackgroundShell,
|
||||
setActiveBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
} = useUIActions();
|
||||
const activeShell = shells.get(activePid);
|
||||
const [output, setOutput] = useState<string | AnsiOutput>(
|
||||
@@ -152,13 +152,13 @@ export const BackgroundTaskDisplay = ({
|
||||
// RadioButtonSelect handles Enter -> onSelect
|
||||
|
||||
if (keyMatchers[Command.BACKGROUND_SHELL_ESCAPE](key)) {
|
||||
setIsBackgroundTaskListOpen(false);
|
||||
setIsBackgroundShellListOpen(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.KILL_BACKGROUND_SHELL](key)) {
|
||||
if (highlightedPid) {
|
||||
void dismissBackgroundTask(highlightedPid);
|
||||
void dismissBackgroundShell(highlightedPid);
|
||||
// If we killed the active one, the list might update via props
|
||||
}
|
||||
return true;
|
||||
@@ -166,9 +166,9 @@ export const BackgroundTaskDisplay = ({
|
||||
|
||||
if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL_LIST](key)) {
|
||||
if (highlightedPid) {
|
||||
setActiveBackgroundTaskPid(highlightedPid);
|
||||
setActiveBackgroundShellPid(highlightedPid);
|
||||
}
|
||||
setIsBackgroundTaskListOpen(false);
|
||||
setIsBackgroundShellListOpen(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -179,12 +179,12 @@ export const BackgroundTaskDisplay = ({
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.KILL_BACKGROUND_SHELL](key)) {
|
||||
void dismissBackgroundTask(activeShell.pid);
|
||||
void dismissBackgroundShell(activeShell.pid);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL_LIST](key)) {
|
||||
setIsBackgroundTaskListOpen(true);
|
||||
setIsBackgroundShellListOpen(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -339,8 +339,8 @@ export const BackgroundTaskDisplay = ({
|
||||
items={items}
|
||||
initialIndex={initialIndex >= 0 ? initialIndex : 0}
|
||||
onSelect={(pid) => {
|
||||
setActiveBackgroundTaskPid(pid);
|
||||
setIsBackgroundTaskListOpen(false);
|
||||
setActiveBackgroundShellPid(pid);
|
||||
setIsBackgroundShellListOpen(false);
|
||||
}}
|
||||
onHighlight={(pid) => setHighlightedPid(pid)}
|
||||
isFocused={isFocused}
|
||||
@@ -198,7 +198,7 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
||||
nightly: false,
|
||||
isTrustedFolder: true,
|
||||
activeHooks: [],
|
||||
isBackgroundTaskVisible: false,
|
||||
isBackgroundShellVisible: false,
|
||||
embeddedShellFocused: false,
|
||||
showIsExpandableHint: false,
|
||||
quota: {
|
||||
@@ -464,7 +464,7 @@ describe('Composer', () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
embeddedShellFocused: true,
|
||||
isBackgroundTaskVisible: true,
|
||||
isBackgroundShellVisible: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
@@ -494,7 +494,7 @@ describe('Composer', () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
embeddedShellFocused: true,
|
||||
isBackgroundTaskVisible: false,
|
||||
isBackgroundShellVisible: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
@@ -35,7 +35,10 @@ vi.mock('./shared/ScrollableList.js', () => ({
|
||||
|
||||
describe('DetailedMessagesDisplay', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useConsoleMessages).mockReturnValue([]);
|
||||
vi.mocked(useConsoleMessages).mockReturnValue({
|
||||
consoleMessages: [],
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
});
|
||||
it('renders nothing when messages are empty', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
@@ -55,7 +58,10 @@ describe('DetailedMessagesDisplay', () => {
|
||||
{ type: 'error', content: 'Error message', count: 1 },
|
||||
{ type: 'debug', content: 'Debug message', count: 1 },
|
||||
];
|
||||
vi.mocked(useConsoleMessages).mockReturnValue(messages);
|
||||
vi.mocked(useConsoleMessages).mockReturnValue({
|
||||
consoleMessages: messages,
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
|
||||
@@ -73,7 +79,10 @@ describe('DetailedMessagesDisplay', () => {
|
||||
const messages: ConsoleMessageItem[] = [
|
||||
{ type: 'error', content: 'Error message', count: 1 },
|
||||
];
|
||||
vi.mocked(useConsoleMessages).mockReturnValue(messages);
|
||||
vi.mocked(useConsoleMessages).mockReturnValue({
|
||||
consoleMessages: messages,
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
|
||||
@@ -89,7 +98,10 @@ describe('DetailedMessagesDisplay', () => {
|
||||
const messages: ConsoleMessageItem[] = [
|
||||
{ type: 'error', content: 'Error message', count: 1 },
|
||||
];
|
||||
vi.mocked(useConsoleMessages).mockReturnValue(messages);
|
||||
vi.mocked(useConsoleMessages).mockReturnValue({
|
||||
consoleMessages: messages,
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
|
||||
@@ -105,7 +117,10 @@ describe('DetailedMessagesDisplay', () => {
|
||||
const messages: ConsoleMessageItem[] = [
|
||||
{ type: 'log', content: 'Repeated message', count: 5 },
|
||||
];
|
||||
vi.mocked(useConsoleMessages).mockReturnValue(messages);
|
||||
vi.mocked(useConsoleMessages).mockReturnValue({
|
||||
consoleMessages: messages,
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={10} width={80} hasFocus={false} />,
|
||||
|
||||
@@ -29,7 +29,7 @@ export const DetailedMessagesDisplay: React.FC<
|
||||
> = ({ maxHeight, width, hasFocus }) => {
|
||||
const scrollableListRef = useRef<ScrollableListRef<ConsoleMessageItem>>(null);
|
||||
|
||||
const consoleMessages = useConsoleMessages();
|
||||
const { consoleMessages } = useConsoleMessages();
|
||||
const config = useConfig();
|
||||
|
||||
const messages = useMemo(() => {
|
||||
|
||||
@@ -8,11 +8,7 @@ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { Footer } from './Footer.js';
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
import {
|
||||
type Config,
|
||||
UserAccountManager,
|
||||
AuthType,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type Config } from '@google/gemini-cli-core';
|
||||
import path from 'node:path';
|
||||
|
||||
// Normalize paths to POSIX slashes for stable cross-platform snapshots.
|
||||
@@ -73,17 +69,14 @@ const defaultProps = {
|
||||
branchName: 'main',
|
||||
};
|
||||
|
||||
const mockConfigPlain = {
|
||||
const mockConfig = {
|
||||
getTargetDir: () => defaultProps.targetDir,
|
||||
getDebugMode: () => false,
|
||||
getModel: () => defaultProps.model,
|
||||
getIdeMode: () => false,
|
||||
isTrustedFolder: () => true,
|
||||
getExtensionRegistryURI: () => undefined,
|
||||
getContentGeneratorConfig: () => ({ authType: undefined }),
|
||||
};
|
||||
|
||||
const mockConfig = mockConfigPlain as unknown as Config;
|
||||
} as unknown as Config;
|
||||
|
||||
const mockSessionStats = {
|
||||
sessionId: 'test-session-id',
|
||||
@@ -441,7 +434,6 @@ describe('<Footer />', () => {
|
||||
|
||||
it('renders footer with all optional sections hidden (minimal footer)', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
@@ -559,7 +551,6 @@ describe('<Footer />', () => {
|
||||
describe('Footer Token Formatting', () => {
|
||||
const renderWithTokens = async (tokens: number) => {
|
||||
const result = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: {
|
||||
@@ -684,75 +675,6 @@ describe('<Footer />', () => {
|
||||
});
|
||||
|
||||
describe('Footer Custom Items', () => {
|
||||
it('renders auth item with email', async () => {
|
||||
const authConfig = {
|
||||
...mockConfigPlain,
|
||||
getContentGeneratorConfig: () => ({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
}),
|
||||
} as unknown as Config;
|
||||
const getCachedAccountSpy = vi
|
||||
.spyOn(UserAccountManager.prototype, 'getCachedGoogleAccount')
|
||||
.mockReturnValue('test@example.com');
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: authConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
currentModel: 'gemini-pro',
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['auth'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('auth');
|
||||
expect(lastFrame()).toContain('test@example.com');
|
||||
unmount();
|
||||
getCachedAccountSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does NOT render auth item when showUserIdentity is false', async () => {
|
||||
const authConfig = {
|
||||
...mockConfigPlain,
|
||||
getContentGeneratorConfig: () => ({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
}),
|
||||
} as unknown as Config;
|
||||
const getCachedAccountSpy = vi
|
||||
.spyOn(UserAccountManager.prototype, 'getCachedGoogleAccount')
|
||||
.mockReturnValue('test@example.com');
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: authConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
currentModel: 'gemini-pro',
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
showUserIdentity: false,
|
||||
footer: {
|
||||
items: ['workspace', 'auth'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('workspace');
|
||||
expect(output).not.toContain('auth');
|
||||
expect(output).not.toContain('test@example.com');
|
||||
unmount();
|
||||
getCachedAccountSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('renders items in the specified order', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
@@ -812,7 +734,6 @@ describe('<Footer />', () => {
|
||||
|
||||
it('handles empty items array', async () => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import {
|
||||
@@ -13,8 +12,6 @@ import {
|
||||
tildeifyPath,
|
||||
getDisplayString,
|
||||
checkExhaustive,
|
||||
AuthType,
|
||||
UserAccountManager,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||
import process from 'node:process';
|
||||
@@ -186,18 +183,6 @@ export const Footer: React.FC<{ copyModeEnabled?: boolean }> = ({
|
||||
const settings = useSettings();
|
||||
const { vimEnabled, vimMode } = useVimMode();
|
||||
|
||||
const authType = config.getContentGeneratorConfig()?.authType;
|
||||
const [email, setEmail] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (authType) {
|
||||
const userAccountManager = new UserAccountManager();
|
||||
setEmail(userAccountManager.getCachedGoogleAccount() ?? undefined);
|
||||
} else {
|
||||
setEmail(undefined);
|
||||
}
|
||||
}, [authType]);
|
||||
|
||||
if (copyModeEnabled) {
|
||||
return <Box height={1} />;
|
||||
}
|
||||
@@ -236,7 +221,6 @@ export const Footer: React.FC<{ copyModeEnabled?: boolean }> = ({
|
||||
errorCount > 0 &&
|
||||
(isFullErrorVerbosity || debugMode || isDevelopment);
|
||||
const displayVimMode = vimEnabled ? vimMode : undefined;
|
||||
|
||||
const items =
|
||||
settings.merged.ui.footer.items ??
|
||||
deriveItemsFromLegacySettings(settings.merged);
|
||||
@@ -401,25 +385,6 @@ export const Footer: React.FC<{ copyModeEnabled?: boolean }> = ({
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'auth': {
|
||||
if (!settings.merged.ui.showUserIdentity) break;
|
||||
if (!authType) break;
|
||||
const displayStr =
|
||||
authType === AuthType.LOGIN_WITH_GOOGLE
|
||||
? (email ?? 'google')
|
||||
: authType;
|
||||
addCol(
|
||||
id,
|
||||
header,
|
||||
() => (
|
||||
<Text color={itemColor} wrap="truncate-end">
|
||||
{displayStr}
|
||||
</Text>
|
||||
),
|
||||
displayStr.length,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'code-changes': {
|
||||
const added = uiState.sessionStats.metrics.files.totalLinesAdded;
|
||||
const removed = uiState.sessionStats.metrics.files.totalLinesRemoved;
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('<FooterConfigDialog />', () => {
|
||||
expect(lastFrame()).toContain('~/project/path');
|
||||
|
||||
// Move focus down to 'code-changes' (which has colored elements)
|
||||
for (let i = 0; i < 9; i++) {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
act(() => {
|
||||
stdin.write('\u001b[B'); // Down arrow
|
||||
});
|
||||
|
||||
@@ -255,7 +255,6 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
||||
<Text color={getColor('code-changes', theme.status.error)}>-4</Text>
|
||||
</Box>
|
||||
),
|
||||
auth: <Text color={getColor('auth', itemColor)}>test@example.com</Text>,
|
||||
'token-count': (
|
||||
<Text color={getColor('token-count', itemColor)}>1.5k tokens</Text>
|
||||
),
|
||||
|
||||
@@ -17,7 +17,6 @@ import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
|
||||
import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
|
||||
import { CompressionMessage } from './messages/CompressionMessage.js';
|
||||
import { WarningMessage } from './messages/WarningMessage.js';
|
||||
import { SubagentHistoryMessage } from './messages/SubagentHistoryMessage.js';
|
||||
import { Box } from 'ink';
|
||||
import { AboutBox } from './AboutBox.js';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
@@ -49,7 +48,6 @@ interface HistoryItemDisplayProps {
|
||||
isExpandable?: boolean;
|
||||
isFirstThinking?: boolean;
|
||||
isFirstAfterThinking?: boolean;
|
||||
suppressNarration?: boolean;
|
||||
}
|
||||
|
||||
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
@@ -62,7 +60,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
isExpandable,
|
||||
isFirstThinking = false,
|
||||
isFirstAfterThinking = false,
|
||||
suppressNarration = false,
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const inlineThinkingMode = getInlineThinkingMode(settings);
|
||||
@@ -71,17 +68,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
const needsTopMarginAfterThinking =
|
||||
isFirstAfterThinking && inlineThinkingMode !== 'off';
|
||||
|
||||
// If there's a topic update in this turn, we suppress the regular narration
|
||||
// and thoughts as they are being "replaced" by the update_topic tool.
|
||||
if (
|
||||
suppressNarration &&
|
||||
(itemForDisplay.type === 'thinking' ||
|
||||
itemForDisplay.type === 'gemini' ||
|
||||
itemForDisplay.type === 'gemini_content')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
@@ -216,12 +202,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
isExpandable={isExpandable}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'subagent' && (
|
||||
<SubagentHistoryMessage
|
||||
item={itemForDisplay}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'compression' && (
|
||||
<CompressionMessage compression={itemForDisplay.compression} />
|
||||
)}
|
||||
|
||||
@@ -1445,101 +1445,9 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Should submit the full command constructed from buffer + suggestion
|
||||
// even if autoExecute is false, because the user explicitly hit Enter on the suggestion.
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('/share');
|
||||
expect(mockCommandCompletion.handleAutocomplete).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should add a space on Tab when command is already complete in the buffer', async () => {
|
||||
const executableCommand: SlashCommand = {
|
||||
name: 'about',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'About info',
|
||||
action: vi.fn(),
|
||||
autoExecute: true,
|
||||
};
|
||||
|
||||
const suggestion = { label: 'about', value: 'about' };
|
||||
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [suggestion],
|
||||
activeSuggestionIndex: 0,
|
||||
getCommandFromSuggestion: vi.fn().mockReturnValue(executableCommand),
|
||||
getCompletedText: vi.fn().mockReturnValue('/about'),
|
||||
});
|
||||
|
||||
// Buffer is already complete
|
||||
props.buffer.setText('/about');
|
||||
props.buffer.lines = ['/about'];
|
||||
props.buffer.cursor = [0, 6];
|
||||
|
||||
const { stdin, unmount } = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
{
|
||||
uiActions,
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\t'); // Tab
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Should call handleAutocomplete which will add the space
|
||||
// Should autocomplete to allow adding file argument
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should submit the base command when Enter is pressed on its suggestion even with a trailing space', async () => {
|
||||
const statsCommand: SlashCommand = {
|
||||
name: 'stats',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'Stats info',
|
||||
action: vi.fn(),
|
||||
autoExecute: false,
|
||||
};
|
||||
|
||||
const suggestion = {
|
||||
label: 'stats',
|
||||
value: 'stats',
|
||||
sectionTitle: 'command',
|
||||
insertValue: 'stats',
|
||||
};
|
||||
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [suggestion],
|
||||
activeSuggestionIndex: 0,
|
||||
getCommandFromSuggestion: vi.fn().mockReturnValue(statsCommand),
|
||||
getCompletedText: vi.fn().mockReturnValue('/stats'),
|
||||
});
|
||||
|
||||
// Buffer has trailing space: "/stats "
|
||||
props.buffer.setText('/stats ');
|
||||
props.buffer.lines = ['/stats '];
|
||||
props.buffer.cursor = [0, 7];
|
||||
|
||||
const { stdin, unmount } = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
{
|
||||
uiActions,
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r'); // Enter
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Should submit "/stats"
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('/stats');
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
@@ -1656,9 +1564,9 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Should submit the command
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('/find-capital');
|
||||
expect(mockCommandCompletion.handleAutocomplete).not.toHaveBeenCalled();
|
||||
// Should autocomplete (not execute) since autoExecute is undefined
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
@@ -2314,67 +2222,85 @@ describe('InputPrompt', () => {
|
||||
name: 'mid-word',
|
||||
text: 'hello world',
|
||||
visualCursor: [0, 3],
|
||||
expected: `hel${chalk.inverse('l')}o world`,
|
||||
},
|
||||
{
|
||||
name: 'at the beginning of the line',
|
||||
text: 'hello',
|
||||
visualCursor: [0, 0],
|
||||
expected: `${chalk.inverse('h')}ello`,
|
||||
},
|
||||
{
|
||||
name: 'at the end of the line',
|
||||
text: 'hello',
|
||||
visualCursor: [0, 5],
|
||||
expected: `hello${chalk.inverse(' ')}`,
|
||||
},
|
||||
{
|
||||
name: 'on a highlighted token',
|
||||
text: 'run @path/to/file',
|
||||
visualCursor: [0, 9],
|
||||
expected: `@path/${chalk.inverse('t')}o/file`,
|
||||
},
|
||||
{
|
||||
name: 'for multi-byte unicode characters',
|
||||
text: 'hello 👍 world',
|
||||
visualCursor: [0, 6],
|
||||
expected: `hello ${chalk.inverse('👍')} world`,
|
||||
},
|
||||
{
|
||||
name: 'after multi-byte unicode characters',
|
||||
text: '👍A',
|
||||
visualCursor: [0, 1],
|
||||
expected: `👍${chalk.inverse('A')}`,
|
||||
},
|
||||
{
|
||||
name: 'at the end of a line with unicode characters',
|
||||
text: 'hello 👍',
|
||||
visualCursor: [0, 8],
|
||||
expected: `hello 👍`, // skip checking inverse ansi due to ink truncation bug
|
||||
},
|
||||
{
|
||||
name: 'at the end of a short line with unicode characters',
|
||||
text: '👍',
|
||||
visualCursor: [0, 1],
|
||||
expected: `👍${chalk.inverse(' ')}`,
|
||||
},
|
||||
{
|
||||
name: 'on an empty line',
|
||||
text: '',
|
||||
visualCursor: [0, 0],
|
||||
expected: chalk.inverse(' '),
|
||||
},
|
||||
{
|
||||
name: 'on a space between words',
|
||||
text: 'hello world',
|
||||
visualCursor: [0, 5],
|
||||
expected: `hello${chalk.inverse(' ')}world`,
|
||||
},
|
||||
])(
|
||||
'should display cursor correctly $name',
|
||||
async ({ text, visualCursor }) => {
|
||||
async ({ name, text, visualCursor, expected }) => {
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = [text];
|
||||
mockBuffer.viewportVisualLines = [text];
|
||||
mockBuffer.visualCursor = visualCursor as [number, number];
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const renderResult = await renderWithProviders(
|
||||
const { stdout, unmount } = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await renderResult.waitUntilReady();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrameRaw();
|
||||
expect(stripAnsi(frame)).toContain(stripAnsi(expected));
|
||||
if (
|
||||
name !== 'at the end of a line with unicode characters' &&
|
||||
name !== 'on a highlighted token'
|
||||
) {
|
||||
expect(frame).toContain('\u001b[7m');
|
||||
}
|
||||
});
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -2390,6 +2316,7 @@ describe('InputPrompt', () => {
|
||||
[1, 0],
|
||||
[2, 0],
|
||||
],
|
||||
expected: `sec${chalk.inverse('o')}nd line`,
|
||||
},
|
||||
{
|
||||
name: 'at the beginning of a line',
|
||||
@@ -2399,6 +2326,7 @@ describe('InputPrompt', () => {
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
],
|
||||
expected: `${chalk.inverse('s')}econd line`,
|
||||
},
|
||||
{
|
||||
name: 'at the end of a line',
|
||||
@@ -2408,10 +2336,11 @@ describe('InputPrompt', () => {
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
],
|
||||
expected: `first line${chalk.inverse(' ')}`,
|
||||
},
|
||||
])(
|
||||
'should display cursor correctly $name in a multiline block',
|
||||
async ({ text, visualCursor, visualToLogicalMap }) => {
|
||||
async ({ name, text, visualCursor, expected, visualToLogicalMap }) => {
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = text.split('\n');
|
||||
mockBuffer.viewportVisualLines = text.split('\n');
|
||||
@@ -2421,12 +2350,20 @@ describe('InputPrompt', () => {
|
||||
>;
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const renderResult = await renderWithProviders(
|
||||
const { stdout, unmount } = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await renderResult.waitUntilReady();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrameRaw();
|
||||
expect(stripAnsi(frame)).toContain(stripAnsi(expected));
|
||||
if (
|
||||
name !== 'at the end of a line with unicode characters' &&
|
||||
name !== 'on a highlighted token'
|
||||
) {
|
||||
expect(frame).toContain('\u001b[7m');
|
||||
}
|
||||
});
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2443,12 +2380,18 @@ describe('InputPrompt', () => {
|
||||
];
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const renderResult = await renderWithProviders(
|
||||
const { stdout, unmount } = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await renderResult.waitUntilReady();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrameRaw();
|
||||
const lines = frame.split('\n');
|
||||
// The line with the cursor should just be an inverted space inside the box border
|
||||
expect(
|
||||
lines.find((l) => l.includes(chalk.inverse(' '))),
|
||||
).not.toBeUndefined();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2469,14 +2412,22 @@ describe('InputPrompt', () => {
|
||||
];
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
|
||||
const renderResult = await renderWithProviders(
|
||||
const { stdout, unmount } = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrameRaw();
|
||||
// Check that all lines, including the empty one, are rendered.
|
||||
// This implicitly tests that the Box wrapper provides height for the empty line.
|
||||
expect(frame).toContain('hello');
|
||||
expect(frame).toContain('world');
|
||||
expect(frame).toContain(chalk.inverse(' '));
|
||||
|
||||
await renderResult.waitUntilReady();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
|
||||
renderResult.unmount();
|
||||
const outputLines = frame.trim().split('\n');
|
||||
// The number of lines should be 2 for the border plus 3 for the content.
|
||||
expect(outputLines.length).toBe(5);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4137,12 +4088,14 @@ describe('InputPrompt', () => {
|
||||
it('should not show inverted cursor when shell is focused', async () => {
|
||||
props.isEmbeddedShellFocused = true;
|
||||
props.focus = false;
|
||||
const renderResult = await renderWithProviders(
|
||||
const { stdout, unmount } = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await renderResult.waitUntilReady();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`);
|
||||
});
|
||||
expect(stdout.lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -232,8 +232,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
terminalWidth,
|
||||
activePtyId,
|
||||
history,
|
||||
backgroundTasks,
|
||||
backgroundTaskHeight,
|
||||
backgroundShells,
|
||||
backgroundShellHeight,
|
||||
shortcutsHelpVisible,
|
||||
} = useUIState();
|
||||
const [suppressCompletion, setSuppressCompletion] = useState(false);
|
||||
@@ -1082,12 +1082,16 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
const command =
|
||||
completion.getCommandFromSuggestion(suggestion);
|
||||
|
||||
const completedText = completion.getCompletedText(suggestion);
|
||||
// Only auto-execute if the command has no completion function
|
||||
// (i.e., it doesn't require an argument to be selected)
|
||||
if (
|
||||
command &&
|
||||
isAutoExecutableCommand(command) &&
|
||||
!command.completion
|
||||
) {
|
||||
const completedText =
|
||||
completion.getCompletedText(suggestion);
|
||||
|
||||
// If the user hits Enter on a suggestion, we should submit it
|
||||
// IF it has an action and doesn't require further arguments (no completion function).
|
||||
// This is separate from autoExecute, which handles automatic execution while typing.
|
||||
if (command && command.action && !command.completion) {
|
||||
if (completedText) {
|
||||
setExpandedSuggestionIndex(-1);
|
||||
handleSubmit(completedText.trim());
|
||||
@@ -1258,7 +1262,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
if (keyMatchers[Command.FOCUS_SHELL_INPUT](key)) {
|
||||
if (
|
||||
activePtyId ||
|
||||
(backgroundTasks.size > 0 && backgroundTaskHeight > 0)
|
||||
(backgroundShells.size > 0 && backgroundShellHeight > 0)
|
||||
) {
|
||||
setEmbeddedShellFocused(true);
|
||||
return true;
|
||||
@@ -1321,8 +1325,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
setBannerVisible,
|
||||
activePtyId,
|
||||
setEmbeddedShellFocused,
|
||||
backgroundTasks.size,
|
||||
backgroundTaskHeight,
|
||||
backgroundShells.size,
|
||||
backgroundShellHeight,
|
||||
streamingState,
|
||||
handleEscPress,
|
||||
registerPlainTabPress,
|
||||
|
||||
@@ -27,10 +27,6 @@ import {
|
||||
} from '../hooks/useConfirmingTool.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('ink-spinner', () => ({
|
||||
default: () => <Text>⠋</Text>,
|
||||
}));
|
||||
|
||||
const mockUseSettings = vi.fn().mockReturnValue({
|
||||
merged: {
|
||||
ui: {
|
||||
@@ -90,10 +86,10 @@ vi.mock('./shared/ScrollableList.js', () => ({
|
||||
}));
|
||||
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { type BackgroundTask } from '../hooks/shellReducer.js';
|
||||
import { type BackgroundShell } from '../hooks/shellReducer.js';
|
||||
|
||||
describe('getToolGroupBorderAppearance', () => {
|
||||
const mockBackgroundTasks = new Map<number, BackgroundTask>();
|
||||
const mockBackgroundShells = new Map<number, BackgroundShell>();
|
||||
const activeShellPtyId = 123;
|
||||
|
||||
it('returns default empty values for non-tool_group items', () => {
|
||||
@@ -103,7 +99,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
null,
|
||||
false,
|
||||
[],
|
||||
mockBackgroundTasks,
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({ borderColor: '', borderDimColor: false });
|
||||
});
|
||||
@@ -148,7 +144,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
null,
|
||||
false,
|
||||
pendingItems,
|
||||
mockBackgroundTasks,
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.border.default,
|
||||
@@ -177,7 +173,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
null,
|
||||
false,
|
||||
[],
|
||||
mockBackgroundTasks,
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.border.default,
|
||||
@@ -206,7 +202,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
null,
|
||||
false,
|
||||
[],
|
||||
mockBackgroundTasks,
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.status.warning,
|
||||
@@ -236,7 +232,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
activeShellPtyId,
|
||||
false,
|
||||
[],
|
||||
mockBackgroundTasks,
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.ui.active,
|
||||
@@ -266,7 +262,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
activeShellPtyId,
|
||||
true,
|
||||
[],
|
||||
mockBackgroundTasks,
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.ui.focus,
|
||||
@@ -295,7 +291,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
activeShellPtyId,
|
||||
false,
|
||||
[],
|
||||
mockBackgroundTasks,
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.ui.active,
|
||||
@@ -312,7 +308,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
activeShellPtyId,
|
||||
true,
|
||||
[],
|
||||
mockBackgroundTasks,
|
||||
mockBackgroundShells,
|
||||
);
|
||||
// Since there are no tools to inspect, it falls back to empty pending, but isCurrentlyInShellTurn=true
|
||||
// so it counts as pending shell.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user