mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 00:01:24 -07:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5d0fc2c3e | |||
| e860f517c0 | |||
| b0be1f1689 | |||
| 85dd6ef773 | |||
| ae672881d1 | |||
| 9e7c10ad88 | |||
| 381669dce0 | |||
| 707b3e85d5 | |||
| 7d36cc004f | |||
| cb4f0c6fa4 | |||
| b0f38104d7 | |||
| 7469ea0fca | |||
| 00fdb30211 | |||
| 0fe8492569 | |||
| 76387d22ae | |||
| a362b7b382 | |||
| 71308caf05 | |||
| 6396ab1ccb | |||
| 12531a06f8 | |||
| d1cd69a20d | |||
| 62346875e4 | |||
| 13e013230b | |||
| bb6a336ca9 | |||
| f14d0c6a17 | |||
| b611f9a519 | |||
| d3bca5d97a | |||
| 94f4e027f8 | |||
| 95b7d69d5b | |||
| f605628624 | |||
| 2238802e97 | |||
| 32cfce16bb | |||
| 137080da45 | |||
| 695785c69d | |||
| d43d772e65 | |||
| 59e3624ada | |||
| fdda3a2399 | |||
| 2de39311cf | |||
| fc926cd0b0 | |||
| 567761fbc0 | |||
| f2591aada6 | |||
| 74468928e8 | |||
| d561b995c0 | |||
| b44e432f55 | |||
| bcc6ee596a | |||
| bc258eba4c | |||
| 0e30055ae4 | |||
| ac7687c1dd | |||
| f9da18e0cc | |||
| f619cd8750 | |||
| 42eedc9184 | |||
| eb17d94e91 | |||
| 9f1623688d | |||
| 7f066fd3a7 | |||
| 6d36219e55 | |||
| 5cedd28733 | |||
| ba3e1ed1c7 | |||
| 7c2cdb225a | |||
| a91f4e692a | |||
| 9bb175a506 | |||
| 0b169e9867 | |||
| 553ae3ad69 | |||
| b2265cfde4 | |||
| bee1267e2a | |||
| 30065c51fb | |||
| 065e69a12b | |||
| 519a0e617a | |||
| 0465de303a | |||
| beaa134f0e | |||
| 47f4a3e50e | |||
| 3787c71d15 | |||
| 9e09db1ddb | |||
| 25ae1a1b54 | |||
| 65accca296 | |||
| 72f0733d98 | |||
| 4d4e16cc8c | |||
| 370b165769 | |||
| 899e8a9edc | |||
| 4dca5c27c6 | |||
| 06c7dc561e | |||
| f54eaa1713 | |||
| 6807a2fa28 | |||
| 9aeeb69ab0 | |||
| 0a77999d73 | |||
| 9035ca32bc | |||
| ef3146eed6 | |||
| ffbecd4eb4 | |||
| 2c09785748 | |||
| 130b596c80 |
@@ -0,0 +1,22 @@
|
||||
description = "Analyze agent behavior and suggest high-level improvements to system prompts."
|
||||
prompt = """
|
||||
# Prompt Engineering Analysis
|
||||
|
||||
You are a world-class prompt engineer and an expert AI engineer at the top of your class. Your goal is to analyze a specific agent behavior or failure and suggest high-level improvements to the system instructions.
|
||||
|
||||
**Observed Behavior / Issue:**
|
||||
{{args}}
|
||||
|
||||
**Reference Context:**
|
||||
- System Prompt Logic: @packages/core/src/core/prompts.ts
|
||||
|
||||
### Task
|
||||
1. **Analyze the Failure:** Review the provided behavior and identify the underlying instructional causes. Use the `/introspect` command output if provided by the user.
|
||||
2. **Strategic Insights:** Share your technical view of the issue. Focus on the "why" and identify any instructional inertia or ambiguity.
|
||||
3. **Propose Improvements:** Suggest high-level changes to the system instructions to prevent this behavior.
|
||||
|
||||
### Principles
|
||||
- **Avoid Hyper-scoping:** Do not create narrow solutions for specific scenarios; aim for generalized improvements that handle classes of behavior.
|
||||
- **Avoid Specific Examples in Suggestions:** Keep the proposed instructions semantic and high-level to prevent the agent from over-indexing on specific cases.
|
||||
- **Maintain Operational Rigor:** Ensure suggestions do not compromise safety, security, or the quality of the agent's work.
|
||||
"""
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"experimental": {
|
||||
"skills": true
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,136 @@
|
||||
|
||||
---
|
||||
name: docs-writer
|
||||
description:
|
||||
Use this skill for writing, reviewing, and editing documentation (`/docs`
|
||||
directory or any .md file) for Gemini CLI.
|
||||
Always use this skill when the task involves writing, reviewing, or editing
|
||||
documentation, specifically for any files in the `/docs` directory or any
|
||||
`.md` files in the repository.
|
||||
---
|
||||
|
||||
# `docs-writer` skill instructions
|
||||
|
||||
As an expert technical writer and editor for the Gemini CLI project, your goal
|
||||
is to produce and refine documentation that is accurate, clear, consistent, and
|
||||
easy for users to understand. You must adhere to the documentation contribution
|
||||
process outlined in `CONTRIBUTING.md`.
|
||||
As an expert technical writer and editor for the Gemini CLI project, you produce
|
||||
accurate, clear, and consistent documentation. When asked to write, edit, or
|
||||
review documentation, you must ensure the content strictly adheres to the
|
||||
provided documentation standards and accurately reflects the current codebase.
|
||||
Adhere to the contribution process in `CONTRIBUTING.md` and the following
|
||||
project standards.
|
||||
|
||||
## Step 1: Understand the goal and create a plan
|
||||
## Phase 1: Documentation standards
|
||||
|
||||
1. **Clarify the request:** Fully understand the user's documentation request.
|
||||
Identify the core feature, command, or concept that needs work.
|
||||
2. **Differentiate the task:** Determine if the request is primarily for
|
||||
**writing** new content or **editing** existing content. If the request is
|
||||
ambiguous (e.g., "fix the docs"), ask the user for clarification.
|
||||
3. **Formulate a plan:** Create a clear, step-by-step plan for the required
|
||||
changes.
|
||||
Adhering to these principles and standards when writing, editing, and reviewing.
|
||||
|
||||
## Step 2: Investigate and gather information
|
||||
### Voice and tone
|
||||
Adopt a tone that balances professionalism with a helpful, conversational
|
||||
approach.
|
||||
|
||||
1. **Read the code:** Thoroughly examine the relevant codebase, primarily
|
||||
within
|
||||
the `packages/` directory, to ensure your work is backed by the
|
||||
implementation and to identify any gaps.
|
||||
2. **Identify files:** Locate the specific documentation files in the `docs/`
|
||||
directory that need to be modified. Always read the latest version of a file
|
||||
before you begin work.
|
||||
3. **Check for connections:** Consider related documentation. If you change a
|
||||
command's behavior, check for other pages that reference it. If you add a new
|
||||
page, check if `docs/sidebar.json` needs to be updated. Make sure all
|
||||
links are up to date.
|
||||
- **Perspective and tense:** Address the reader as "you." Use active voice and
|
||||
present tense (e.g., "The API returns...").
|
||||
- **Tone:** Professional, friendly, and direct.
|
||||
- **Clarity:** Use simple vocabulary. Avoid jargon, slang, and marketing hype.
|
||||
- **Global Audience:** Write in standard US English. Avoid idioms and cultural
|
||||
references.
|
||||
- **Requirements:** Be clear about requirements ("must") vs. recommendations
|
||||
("we recommend"). Avoid "should."
|
||||
- **Word Choice:** Avoid "please" and anthropomorphism (e.g., "the server
|
||||
thinks"). Use contractions (don't, it's).
|
||||
|
||||
## Step 3: Write or edit the documentation
|
||||
### Language and grammar
|
||||
Write precisely to ensure your instructions are unambiguous.
|
||||
|
||||
1. **Follow the style guide:** Adhere to the rules in
|
||||
`references/style-guide.md`. Read this file to understand the project's
|
||||
documentation standards.
|
||||
2. Ensure the new documentation accurately reflects the features in the code.
|
||||
3. **Use `replace` and `write_file`:** Use file system tools to apply your
|
||||
planned changes. For small edits, `replace` is preferred. For new files or
|
||||
large rewrites, `write_file` is more appropriate.
|
||||
- **Abbreviations:** Avoid Latin abbreviations; use "for example" (not "e.g.")
|
||||
and "that is" (not "i.e.").
|
||||
- **Punctuation:** Use the serial comma. Place periods and commas inside
|
||||
quotation marks.
|
||||
- **Dates:** Use unambiguous formats (e.g., "January 22, 2026").
|
||||
- **Conciseness:** Use "lets you" instead of "allows you to." Use precise,
|
||||
specific verbs.
|
||||
- **Examples:** Use meaningful names in examples; avoid placeholders like
|
||||
"foo" or "bar."
|
||||
|
||||
### Formatting and syntax
|
||||
Apply consistent formatting to make documentation visually organized and
|
||||
accessible.
|
||||
|
||||
- **Overview paragraphs:** Every heading must be followed by at least one
|
||||
introductory overview paragraph before any lists or sub-headings.
|
||||
- **Text wrap:** Wrap text at 80 characters (except long links or tables).
|
||||
- **Casing:** Use sentence case for headings, titles, and bolded text.
|
||||
- **Naming:** Always refer to the project as `Gemini CLI` (never
|
||||
`the Gemini CLI`).
|
||||
- **Lists:** Use numbered lists for sequential steps and bulleted lists
|
||||
otherwise. Keep list items parallel in structure.
|
||||
- **UI and code:** Use **bold** for UI elements and `code font` for filenames,
|
||||
snippets, commands, and API elements. Focus on the task when discussing
|
||||
interaction.
|
||||
- **Links:** Use descriptive anchor text; avoid "click here." Ensure the link
|
||||
makes sense out of context.
|
||||
- **Accessibility:** Use semantic HTML elements correctly (headings, lists,
|
||||
tables).
|
||||
- **Media:** Use lowercase hyphenated filenames. Provide descriptive alt text
|
||||
for all images.
|
||||
|
||||
### Structure
|
||||
- **BLUF:** Start with an introduction explaining what to expect.
|
||||
- **Experimental features:** If a feature is clearly noted as experimental,
|
||||
add the following note immediately after the introductory paragraph:
|
||||
`> **Note:** This is a preview feature currently under active development.`
|
||||
- **Headings:** Use hierarchical headings to support the user journey.
|
||||
- **Procedures:**
|
||||
- Introduce lists of steps with a complete sentence.
|
||||
- Start each step with an imperative verb.
|
||||
- Number sequential steps; use bullets for non-sequential lists.
|
||||
- Put conditions before instructions (e.g., "On the Settings page, click...").
|
||||
- Provide clear context for where the action takes place.
|
||||
- Indicate optional steps clearly (e.g., "Optional: ...").
|
||||
- **Elements:** Use bullet lists, tables, notes (`> **Note:**`), and warnings
|
||||
(`> **Warning:**`).
|
||||
- **Avoid using a table of contents:** If a table of contents is present, remove
|
||||
it.
|
||||
- **Next steps:** Conclude with a "Next steps" section if applicable.
|
||||
|
||||
## Phase 2: Preparation
|
||||
Before modifying any documentation, thoroughly investigate the request and the
|
||||
surrounding context.
|
||||
|
||||
1. **Clarify:** Understand the core request. Differentiate between writing new
|
||||
content and editing existing content. If the request is ambiguous (e.g.,
|
||||
"fix the docs"), ask for clarification.
|
||||
2. **Investigate:** Examine relevant code (primarily in `packages/`) for
|
||||
accuracy.
|
||||
3. **Audit:** Read the latest versions of relevant files in `docs/`.
|
||||
4. **Connect:** Identify all referencing pages if changing behavior. Check if
|
||||
`docs/sidebar.json` needs updates.
|
||||
5. **Plan:** Create a step-by-step plan before making changes.
|
||||
|
||||
## Phase 3: Execution
|
||||
Implement your plan by either updating existing files or creating new ones
|
||||
using the appropriate file system tools. Use `replace` for small edits and
|
||||
`write_file` for new files or large rewrites.
|
||||
|
||||
### Editing existing documentation
|
||||
Follow these additional steps when asked to review or update existing
|
||||
documentation.
|
||||
|
||||
- **Gaps:** Identify areas where the documentation is incomplete or no longer
|
||||
reflects existing code.
|
||||
- **Structure:** Apply "Structure (New Docs)" rules (BLUF, headings, etc.) when
|
||||
adding new sections to existing pages.
|
||||
- **Tone:** Ensure the tone is active and engaging. Use "you" and contractions.
|
||||
- **Clarity:** Correct awkward wording, spelling, and grammar. Rephrase
|
||||
sentences to make them easier for users to understand.
|
||||
- **Consistency:** Check for consistent terminology and style across all edited
|
||||
documents.
|
||||
|
||||
|
||||
## Phase 4: Verification and finalization
|
||||
Perform a final quality check to ensure that all changes are correctly formatted
|
||||
and that all links are functional.
|
||||
|
||||
### Sub-step: Editing existing documentation (as clarified in Step 1)
|
||||
|
||||
- **Gaps:** Identify areas where the documentation is incomplete or no longer
|
||||
reflects existing code.
|
||||
- **Tone:** Ensure the tone is active and engaging, not passive.
|
||||
- **Clarity:** Correct awkward wording, spelling, and grammar. Rephrase
|
||||
sentences to make them easier for users to understand.
|
||||
- **Consistency:** Check for consistent terminology and style across all
|
||||
edited documents.
|
||||
|
||||
## Step 4: Verify and finalize
|
||||
|
||||
1. **Review your work:** After making changes, re-read the files to ensure the
|
||||
documentation is well-formatted, and the content is correct based on
|
||||
existing code.
|
||||
2. **Link verification:** Verify the validity of all links in the new content.
|
||||
Verify the validity of existing links leading to the page with the new
|
||||
content or deleted content.
|
||||
2. **Offer to run npm format:** Once all changes are complete, offer to run the
|
||||
project's formatting script to ensure consistency by proposing the command:
|
||||
`npm run format`
|
||||
1. **Accuracy:** Ensure content accurately reflects the implementation and
|
||||
technical behavior.
|
||||
2. **Self-review:** Re-read changes for formatting, correctness, and flow.
|
||||
3. **Link check:** Verify all new and existing links leading to or from modified
|
||||
pages.
|
||||
4. **Format:** Once all changes are complete, ask to execute `npm run format`
|
||||
to ensure consistent formatting across the project. If the user confirms,
|
||||
execute the command.
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
# Documentation style guide
|
||||
|
||||
## I. Core principles
|
||||
|
||||
1. **Clarity:** Write for easy understanding. Prioritize clear, direct, and
|
||||
simple language.
|
||||
2. **Consistency:** Use consistent terminology, formatting, and style
|
||||
throughout the documentation.
|
||||
3. **Accuracy:** Ensure all information is technically correct and up-to-date.
|
||||
4. **Accessibility:** Design documentation to be usable by everyone. Focus on
|
||||
semantic structure, clear link text, and image alternatives.
|
||||
5. **Global audience:** Write in standard US English. Avoid slang, idioms, and
|
||||
cultural references.
|
||||
6. **Prescriptive:** Guide the reader by recommending specific actions and
|
||||
paths, especially for complex tasks.
|
||||
|
||||
## II. Voice and tone
|
||||
|
||||
- **Professional yet friendly:** Maintain a helpful, knowledgeable, and
|
||||
conversational tone without being frivolous.
|
||||
- **Direct:** Get straight to the point. Keep paragraphs short and focused.
|
||||
- **Second person:** Address the reader as "you."
|
||||
- **Present tense:** Use the present tense to describe functionality (e.g., "The
|
||||
API returns a JSON object.").
|
||||
- **Avoid:** Jargon, slang, marketing hype, and overly casual language.
|
||||
|
||||
## III. Language and grammar
|
||||
|
||||
- **Active voice:** Prefer active voice over passive voice.
|
||||
- _Example:_ "The system sends a notification." (Not: "A notification is sent
|
||||
by the system.")
|
||||
- **Contractions:** Use common contractions (e.g., "don't," "it's") to maintain
|
||||
a natural tone.
|
||||
- **Simple vocabulary:** Use common words. Define technical terms when
|
||||
necessary.
|
||||
- **Conciseness:** Keep sentences short and focused.
|
||||
- **"Please":** Avoid using the word "please."
|
||||
|
||||
## IV. Procedures and steps
|
||||
|
||||
- Start each step with an imperative verb (e.g., "Connect to the database").
|
||||
- Number steps sequentially.
|
||||
- Introduce lists of steps with a complete sentence.
|
||||
- Put conditions before instructions, not after.
|
||||
- Provide clear context for where the action takes place (e.g., "In the
|
||||
administration console...").
|
||||
- Indicate optional steps clearly (e.g., "Optional: ...").
|
||||
|
||||
## V. Formatting and punctuation
|
||||
|
||||
- **Text wrap:** Wrap all text at 80 characters, with exceptions for long links
|
||||
or tables.
|
||||
- **Headings, titles, and bold text:** Use sentence case. Structure headings
|
||||
hierarchically.
|
||||
- **Lists:** Use numbered lists for sequential steps and bulleted lists for all
|
||||
other lists. Keep list items parallel in structure.
|
||||
- **Serial comma:** Use the serial comma (e.g., "one, two, and three").
|
||||
- **Punctuation:** Use standard American punctuation. Place periods inside
|
||||
quotation marks.
|
||||
- **Dates:** Use unambiguous date formatting (e.g., "January 22, 2026").
|
||||
|
||||
## VI. UI, code, and links
|
||||
|
||||
- **UI elements:** Put UI elements in **bold**. Focus on the task when
|
||||
discussing interaction.
|
||||
- **Code:** Use `code font` for filenames, code snippets, commands, and API
|
||||
elements. Use code blocks for multi-line samples.
|
||||
- **Links:** Use descriptive link text that indicates what the link leads to.
|
||||
Avoid "click here."
|
||||
|
||||
## VII. Word choice and terminology
|
||||
|
||||
- **Consistent naming:** Use product and feature names consistently. Always
|
||||
refer to Gemini CLI as `Gemini CLI`, never `the Gemini CLI`.
|
||||
- **Specific verbs:** Use precise verbs.
|
||||
- **Avoid:**
|
||||
- Latin abbreviations (e.g., use "for example" instead of "e.g.").
|
||||
- Placeholder names like "foo" and "bar" in examples; use meaningful names
|
||||
instead.
|
||||
- Anthropomorphism (e.g., "The server thinks...").
|
||||
- "Should": Be clear about requirements ("must") vs. recommendations ("we
|
||||
recommend").
|
||||
|
||||
## VIII. Files and media
|
||||
|
||||
- **Filenames:** Use lowercase letters, separate words with hyphens (-), and use
|
||||
standard ASCII characters.
|
||||
- **Images:** Provide descriptive alt text for all images. Provide
|
||||
high-resolution or vector images when practical.
|
||||
|
||||
## IX. Accessibility quick check
|
||||
|
||||
- Provide descriptive alt text for images.
|
||||
- Ensure link text makes sense out of context.
|
||||
- Use semantic HTML elements correctly (headings, lists, tables).
|
||||
@@ -28,7 +28,7 @@ body:
|
||||
id: 'info'
|
||||
attributes:
|
||||
label: 'Client information'
|
||||
description: 'Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (macOS, Windows, Linux).'
|
||||
description: 'Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (macOS, Windows, Linux). Note that this output contains your email address. Consider removing it before submitting.'
|
||||
value: |-
|
||||
<details>
|
||||
<summary>Client Information</summary>
|
||||
|
||||
@@ -177,6 +177,19 @@ jobs:
|
||||
- name: 'Smoke test bundle'
|
||||
run: 'node ./bundle/gemini.js --version'
|
||||
|
||||
- name: 'Smoke test npx installation'
|
||||
run: |
|
||||
# 1. Package the project into a tarball
|
||||
TARBALL=$(npm pack | tail -n 1)
|
||||
|
||||
# 2. Move to a fresh directory for isolation
|
||||
mkdir -p ../smoke-test-dir
|
||||
mv "$TARBALL" ../smoke-test-dir/
|
||||
cd ../smoke-test-dir
|
||||
|
||||
# 3. Run npx from the tarball
|
||||
npx "./$TARBALL" --version
|
||||
|
||||
- name: 'Wait for file system sync'
|
||||
run: 'sleep 2'
|
||||
|
||||
@@ -252,6 +265,19 @@ jobs:
|
||||
- name: 'Smoke test bundle'
|
||||
run: 'node ./bundle/gemini.js --version'
|
||||
|
||||
- name: 'Smoke test npx installation'
|
||||
run: |
|
||||
# 1. Package the project into a tarball
|
||||
TARBALL=$(npm pack | tail -n 1)
|
||||
|
||||
# 2. Move to a fresh directory for isolation
|
||||
mkdir -p ../smoke-test-dir
|
||||
mv "$TARBALL" ../smoke-test-dir/
|
||||
cd ../smoke-test-dir
|
||||
|
||||
# 3. Run npx from the tarball
|
||||
npx "./$TARBALL" --version
|
||||
|
||||
- name: 'Wait for file system sync'
|
||||
run: 'sleep 2'
|
||||
|
||||
@@ -396,6 +422,21 @@ jobs:
|
||||
run: 'node ./bundle/gemini.js --version'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Smoke test npx installation'
|
||||
run: |
|
||||
# 1. Package the project into a tarball
|
||||
$PACK_OUTPUT = npm pack
|
||||
$TARBALL = $PACK_OUTPUT[-1]
|
||||
|
||||
# 2. Move to a fresh directory for isolation
|
||||
New-Item -ItemType Directory -Force -Path ../smoke-test-dir
|
||||
Move-Item $TARBALL ../smoke-test-dir/
|
||||
Set-Location ../smoke-test-dir
|
||||
|
||||
# 3. Run npx from the tarball
|
||||
npx "./$TARBALL" --version
|
||||
shell: 'pwsh'
|
||||
|
||||
ci:
|
||||
name: 'CI'
|
||||
if: 'always()'
|
||||
|
||||
@@ -43,19 +43,23 @@ jobs:
|
||||
|
||||
// 1. Fetch maintainers for verification
|
||||
let maintainerLogins = new Set();
|
||||
let teamFetchSucceeded = false;
|
||||
try {
|
||||
const members = await github.paginate(github.rest.teams.listMembersInOrg, {
|
||||
org: context.repo.owner,
|
||||
team_slug: 'gemini-cli-maintainers'
|
||||
});
|
||||
maintainerLogins = new Set(members.map(m => m.login));
|
||||
maintainerLogins = new Set(members.map(m => m.login.toLowerCase()));
|
||||
teamFetchSucceeded = true;
|
||||
core.info(`Successfully fetched ${maintainerLogins.size} team members from gemini-cli-maintainers`);
|
||||
} catch (e) {
|
||||
core.warning('Failed to fetch team members');
|
||||
core.warning(`Failed to fetch team members from gemini-cli-maintainers: ${e.message}. Falling back to author_association only.`);
|
||||
}
|
||||
|
||||
const isMaintainer = (login, assoc) => {
|
||||
if (maintainerLogins.size > 0) return maintainerLogins.has(login);
|
||||
return ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
|
||||
const isTeamMember = maintainerLogins.has(login.toLowerCase());
|
||||
const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
|
||||
return isTeamMember || isRepoMaintainer;
|
||||
};
|
||||
|
||||
// 2. Determine which PRs to check
|
||||
@@ -153,7 +157,17 @@ jobs:
|
||||
const labels = pr.labels.map(l => l.name.toLowerCase());
|
||||
if (labels.includes('help wanted') || labels.includes('🔒 maintainer only')) continue;
|
||||
|
||||
let lastActivity = new Date(0);
|
||||
// Skip PRs that were created less than 30 days ago - they cannot be stale yet
|
||||
const prCreatedAt = new Date(pr.created_at);
|
||||
if (prCreatedAt > thirtyDaysAgo) {
|
||||
const daysOld = Math.floor((Date.now() - prCreatedAt.getTime()) / (1000 * 60 * 60 * 24));
|
||||
core.info(`PR #${pr.number} was created ${daysOld} days ago. Skipping staleness check.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialize lastActivity to PR creation date (not epoch) as a safety baseline.
|
||||
// This ensures we never incorrectly mark a PR as stale due to failed activity lookups.
|
||||
let lastActivity = new Date(pr.created_at);
|
||||
try {
|
||||
const reviews = await github.paginate(github.rest.pulls.listReviews, {
|
||||
owner: context.repo.owner,
|
||||
@@ -177,8 +191,12 @@ jobs:
|
||||
if (d > lastActivity) lastActivity = d;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
core.warning(`Failed to fetch reviews/comments for PR #${pr.number}: ${e.message}`);
|
||||
}
|
||||
|
||||
// For maintainer PRs, the PR creation itself counts as maintainer activity.
|
||||
// (Now redundant since we initialize to pr.created_at, but kept for clarity)
|
||||
if (maintainerPr) {
|
||||
const d = new Date(pr.created_at);
|
||||
if (d > lastActivity) lastActivity = d;
|
||||
|
||||
@@ -72,7 +72,8 @@ powerful tool for developers.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Always use the `docs-writer` skill when you are asked to write, edit, or
|
||||
review any documentation.
|
||||
- Documentation is located in the `docs/` directory.
|
||||
- Suggest documentation updates when code changes render existing documentation
|
||||
obsolete or incomplete.
|
||||
- Located in the `docs/` directory.
|
||||
- Use the `docs-writer` skill.
|
||||
|
||||
@@ -18,6 +18,23 @@ on GitHub.
|
||||
| [Preview](preview.md) | Experimental features ready for early feedback. |
|
||||
| [Stable](latest.md) | Stable, recommended for general use. |
|
||||
|
||||
## Announcements: v0.26.0 - 2026-01-27
|
||||
|
||||
- **Agents and Skills:** We've introduced a new `skill-creator` skill
|
||||
([#16394](https://github.com/google-gemini/gemini-cli/pull/16394) by
|
||||
@NTaylorMullen), enabled agent skills by default, and added a generalist agent
|
||||
to improve task routing
|
||||
([#16638](https://github.com/google-gemini/gemini-cli/pull/16638) by
|
||||
@joshualitt).
|
||||
- **UI/UX Improvements:** You can now "Rewind" through your conversation history
|
||||
([#15717](https://github.com/google-gemini/gemini-cli/pull/15717) by @Adib234)
|
||||
and use a new `/introspect` command for debugging.
|
||||
- **Core and Scheduler Refactoring:** The core scheduler has been significantly
|
||||
refactored to improve performance and reliability
|
||||
([#16895](https://github.com/google-gemini/gemini-cli/pull/16895) by
|
||||
@abhipatel12), and numerous performance and stability fixes have been
|
||||
included.
|
||||
|
||||
## Announcements: v0.25.0 - 2026-01-20
|
||||
|
||||
- **Skills and Agents Improvements:** We've enhanced the `activate_skill` tool,
|
||||
|
||||
+315
-347
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.25.0
|
||||
# Latest stable release: v0.26.0
|
||||
|
||||
Released: January 20, 2026
|
||||
Released: January 27, 2026
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -11,360 +11,328 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Skills and Agents Improvements:** Enhanced `activate_skill` tool, new
|
||||
`pr-creator` skill, default enablement of skills, improved `cli_help` agent,
|
||||
and a new `/agents refresh` command.
|
||||
- **UI/UX Refinements:** Transparent feedback for skills, ability to switch
|
||||
focus between shell and input with Tab, and dynamic terminal tab titles.
|
||||
- **Core Functionality & Performance:** Support for built-in agent skills,
|
||||
refined Gemini 3 system instructions, caching ignore instances for
|
||||
performance, and improved retry mechanisms.
|
||||
- **Bug Fixes and Stability:** Numerous bug fixes across the CLI, core, and
|
||||
workflows, including issues with subagent delegation, unicode character
|
||||
crashes, and sticky header regressions.
|
||||
- **Enhanced Agent and Skill Capabilities:** This release introduces the new
|
||||
`skill-creator` built-in skill, enables Agent Skills by default, and adds a
|
||||
generalist agent to improve task routing. Security for skill installation has
|
||||
also been enhanced with new consent prompts.
|
||||
- **Improved UI and UX:** A new "Rewind" feature lets you walk back through
|
||||
conversation history. We've also added an `/introspect` command for debugging
|
||||
and unified various shell confirmation dialogs for a more consistent user
|
||||
experience.
|
||||
- **Core Stability and Performance:** This release includes significant
|
||||
performance improvements, including a fix for PDF token estimation,
|
||||
optimizations for large inputs, and prevention of OOM crashes. Key memory
|
||||
management components like `LRUCache` have also been updated.
|
||||
- **Scheduler and Policy Refactoring:** The core tool scheduler has been
|
||||
decoupled into distinct orchestration, policy, and confirmation components,
|
||||
and we've added an experimental event-driven scheduler to improve performance
|
||||
and reliability.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- feat(core): improve activate_skill tool and use lowercase XML tags by
|
||||
@NTaylorMullen in
|
||||
[#16009](https://github.com/google-gemini/gemini-cli/pull/16009)
|
||||
- Add initiation method telemetry property by @gundermanc in
|
||||
[#15818](https://github.com/google-gemini/gemini-cli/pull/15818)
|
||||
- chore(release): bump version to 0.25.0-nightly.20260107.59a18e710 by
|
||||
- fix: PDF token estimation (#16494) by @korade-krushna in
|
||||
[#16527](https://github.com/google-gemini/gemini-cli/pull/16527)
|
||||
- chore(release): bump version to 0.26.0-nightly.20260114.bb6c57414 by
|
||||
@gemini-cli-robot in
|
||||
[#16048](https://github.com/google-gemini/gemini-cli/pull/16048)
|
||||
- Hx support by @kevinfjiang in
|
||||
[#16032](https://github.com/google-gemini/gemini-cli/pull/16032)
|
||||
- [Skills] Foundation: Centralize management logic and feedback rendering by
|
||||
@NTaylorMullen in
|
||||
[#15952](https://github.com/google-gemini/gemini-cli/pull/15952)
|
||||
- Introduce GEMINI_CLI_HOME for strict test isolation by @NTaylorMullen in
|
||||
[#15907](https://github.com/google-gemini/gemini-cli/pull/15907)
|
||||
- [Skills] Multi-scope skill enablement and shadowing fix by @NTaylorMullen in
|
||||
[#15953](https://github.com/google-gemini/gemini-cli/pull/15953)
|
||||
- policy: extract legacy policy from core tool scheduler to policy engine by
|
||||
[#16604](https://github.com/google-gemini/gemini-cli/pull/16604)
|
||||
- docs: clarify F12 to open debug console by @jackwotherspoon in
|
||||
[#16570](https://github.com/google-gemini/gemini-cli/pull/16570)
|
||||
- docs: Remove .md extension from internal links in architecture.md by
|
||||
@medic-code in
|
||||
[#12899](https://github.com/google-gemini/gemini-cli/pull/12899)
|
||||
- Add an experimental setting for extension config by @chrstnb in
|
||||
[#16506](https://github.com/google-gemini/gemini-cli/pull/16506)
|
||||
- feat: add Rewind Confirmation dialog and Rewind Viewer component by @Adib234
|
||||
in [#15717](https://github.com/google-gemini/gemini-cli/pull/15717)
|
||||
- fix(a2a): Don't throw errors for GeminiEventType Retry and InvalidStream. by
|
||||
@ehedlund in [#16541](https://github.com/google-gemini/gemini-cli/pull/16541)
|
||||
- prefactor: add rootCommands as array so it can be used for policy parsing by
|
||||
@abhipatel12 in
|
||||
[#15902](https://github.com/google-gemini/gemini-cli/pull/15902)
|
||||
- Enhance TestRig with process management and timeouts by @NTaylorMullen in
|
||||
[#15908](https://github.com/google-gemini/gemini-cli/pull/15908)
|
||||
- Update troubleshooting doc for UNABLE_TO_GET_ISSUER_CERT_LOCALLY by @sehoon38
|
||||
in [#16069](https://github.com/google-gemini/gemini-cli/pull/16069)
|
||||
- Add keytar to dependencies by @chrstnb in
|
||||
[#15928](https://github.com/google-gemini/gemini-cli/pull/15928)
|
||||
- Simplify extension settings command by @chrstnb in
|
||||
[#16001](https://github.com/google-gemini/gemini-cli/pull/16001)
|
||||
- feat(admin): implement extensions disabled by @skeshive in
|
||||
[#16024](https://github.com/google-gemini/gemini-cli/pull/16024)
|
||||
- Core data structure updates for Rewind functionality by @Adib234 in
|
||||
[#15714](https://github.com/google-gemini/gemini-cli/pull/15714)
|
||||
- feat(hooks): simplify hook firing with HookSystem wrapper methods by @ved015
|
||||
in [#15982](https://github.com/google-gemini/gemini-cli/pull/15982)
|
||||
- Add exp.gws_experiment field to LogEventEntry by @gsquared94 in
|
||||
[#16062](https://github.com/google-gemini/gemini-cli/pull/16062)
|
||||
- Revert "feat(admin): implement extensions disabled" by @chrstnb in
|
||||
[#16082](https://github.com/google-gemini/gemini-cli/pull/16082)
|
||||
- feat(core): Decouple enabling hooks UI from subsystem. by @joshualitt in
|
||||
[#16074](https://github.com/google-gemini/gemini-cli/pull/16074)
|
||||
- docs: add docs for hooks + extensions by @abhipatel12 in
|
||||
[#16073](https://github.com/google-gemini/gemini-cli/pull/16073)
|
||||
- feat(core): Preliminary changes for subagent model routing. by @joshualitt in
|
||||
[#16035](https://github.com/google-gemini/gemini-cli/pull/16035)
|
||||
- Optimize CI workflow: Parallelize jobs and cache linters by @NTaylorMullen in
|
||||
[#16054](https://github.com/google-gemini/gemini-cli/pull/16054)
|
||||
- Add option to fallback for capacity errors in ProQuotaDi… by @sehoon38 in
|
||||
[#16050](https://github.com/google-gemini/gemini-cli/pull/16050)
|
||||
- feat: add confirmation details support + jsonrpc vs http rest support by
|
||||
@adamfweidman in
|
||||
[#16079](https://github.com/google-gemini/gemini-cli/pull/16079)
|
||||
- fix(workflows): fix and limit labels for pr-triage.sh script by @jacob314 in
|
||||
[#16096](https://github.com/google-gemini/gemini-cli/pull/16096)
|
||||
- Fix and rename introspection agent -> cli help agent by @scidomino in
|
||||
[#16097](https://github.com/google-gemini/gemini-cli/pull/16097)
|
||||
- Docs: Changelogs update 20260105 by @jkcinouye in
|
||||
[#15937](https://github.com/google-gemini/gemini-cli/pull/15937)
|
||||
- enable cli_help agent by default by @scidomino in
|
||||
[#16100](https://github.com/google-gemini/gemini-cli/pull/16100)
|
||||
- Optimize json-output tests with mock responses by @NTaylorMullen in
|
||||
[#16102](https://github.com/google-gemini/gemini-cli/pull/16102)
|
||||
- Fix CI for forks by @scidomino in
|
||||
[#16113](https://github.com/google-gemini/gemini-cli/pull/16113)
|
||||
- Reduce nags about PRs that reference issues but don't fix them. by @jacob314
|
||||
in [#16112](https://github.com/google-gemini/gemini-cli/pull/16112)
|
||||
- feat(cli): add filepath autosuggestion after slash commands by @jasmeetsb in
|
||||
[#14738](https://github.com/google-gemini/gemini-cli/pull/14738)
|
||||
- Add upgrade option for paid users by @cayden-google in
|
||||
[#15978](https://github.com/google-gemini/gemini-cli/pull/15978)
|
||||
- [Skills] UX Polishing: Transparent feedback and CLI refinements by
|
||||
[#16640](https://github.com/google-gemini/gemini-cli/pull/16640)
|
||||
- remove unnecessary \x7f key bindings by @scidomino in
|
||||
[#16646](https://github.com/google-gemini/gemini-cli/pull/16646)
|
||||
- docs(skills): use body-file in pr-creator skill for better reliability by
|
||||
@abhipatel12 in
|
||||
[#16642](https://github.com/google-gemini/gemini-cli/pull/16642)
|
||||
- chore(automation): recursive labeling for workstream descendants by @bdmorgan
|
||||
in [#16609](https://github.com/google-gemini/gemini-cli/pull/16609)
|
||||
- feat: introduce 'skill-creator' built-in skill and CJS management tools by
|
||||
@NTaylorMullen in
|
||||
[#15954](https://github.com/google-gemini/gemini-cli/pull/15954)
|
||||
- Polish: Move 'Failed to load skills' warning to debug logs by @NTaylorMullen
|
||||
in [#16142](https://github.com/google-gemini/gemini-cli/pull/16142)
|
||||
- feat(cli): export chat history in /bug and prefill GitHub issue by
|
||||
[#16394](https://github.com/google-gemini/gemini-cli/pull/16394)
|
||||
- chore(automation): remove automated PR size and complexity labeler by
|
||||
@bdmorgan in [#16648](https://github.com/google-gemini/gemini-cli/pull/16648)
|
||||
- refactor(skills): replace 'project' with 'workspace' scope by @NTaylorMullen
|
||||
in [#16380](https://github.com/google-gemini/gemini-cli/pull/16380)
|
||||
- Docs: Update release notes for 1/13/2026 by @jkcinouye in
|
||||
[#16583](https://github.com/google-gemini/gemini-cli/pull/16583)
|
||||
- Simplify paste handling by @scidomino in
|
||||
[#16654](https://github.com/google-gemini/gemini-cli/pull/16654)
|
||||
- chore(automation): improve scheduled issue triage discovery and throughput by
|
||||
@bdmorgan in [#16652](https://github.com/google-gemini/gemini-cli/pull/16652)
|
||||
- fix(acp): run exit cleanup when stdin closes by @codefromthecrypt in
|
||||
[#14953](https://github.com/google-gemini/gemini-cli/pull/14953)
|
||||
- feat(scheduler): add types needed for event driven scheduler by @abhipatel12
|
||||
in [#16641](https://github.com/google-gemini/gemini-cli/pull/16641)
|
||||
- Remove unused rewind key binding by @scidomino in
|
||||
[#16659](https://github.com/google-gemini/gemini-cli/pull/16659)
|
||||
- Remove sequence binding by @scidomino in
|
||||
[#16664](https://github.com/google-gemini/gemini-cli/pull/16664)
|
||||
- feat(cli): undeprecate the --prompt flag by @alexaustin007 in
|
||||
[#13981](https://github.com/google-gemini/gemini-cli/pull/13981)
|
||||
- chore: update dependabot configuration by @cosmopax in
|
||||
[#13507](https://github.com/google-gemini/gemini-cli/pull/13507)
|
||||
- feat(config): add 'auto' alias for default model selection by @sehoon38 in
|
||||
[#16661](https://github.com/google-gemini/gemini-cli/pull/16661)
|
||||
- Enable & disable agents by @sehoon38 in
|
||||
[#16225](https://github.com/google-gemini/gemini-cli/pull/16225)
|
||||
- cleanup: Improve keybindings by @scidomino in
|
||||
[#16672](https://github.com/google-gemini/gemini-cli/pull/16672)
|
||||
- Add timeout for shell-utils to prevent hangs. by @jacob314 in
|
||||
[#16667](https://github.com/google-gemini/gemini-cli/pull/16667)
|
||||
- feat(plan): add experimental plan flag by @jerop in
|
||||
[#16650](https://github.com/google-gemini/gemini-cli/pull/16650)
|
||||
- feat(cli): add security consent prompts for skill installation by
|
||||
@NTaylorMullen in
|
||||
[#16115](https://github.com/google-gemini/gemini-cli/pull/16115)
|
||||
- bug(core): fix issue with overrides to bases. by @joshualitt in
|
||||
[#15255](https://github.com/google-gemini/gemini-cli/pull/15255)
|
||||
- enableInteractiveShell for external tooling relying on a2a server by
|
||||
@DavidAPierce in
|
||||
[#16080](https://github.com/google-gemini/gemini-cli/pull/16080)
|
||||
- Reapply "feat(admin): implement extensions disabled" (#16082) by @skeshive in
|
||||
[#16109](https://github.com/google-gemini/gemini-cli/pull/16109)
|
||||
- bug(core): Fix spewie getter in hookTranslator.ts by @joshualitt in
|
||||
[#16108](https://github.com/google-gemini/gemini-cli/pull/16108)
|
||||
- feat(hooks): add mcp_context to BeforeTool and AfterTool hook inputs by @vrv
|
||||
in [#15656](https://github.com/google-gemini/gemini-cli/pull/15656)
|
||||
- Add extension linking capabilities in cli by @kevinjwang1 in
|
||||
[#16040](https://github.com/google-gemini/gemini-cli/pull/16040)
|
||||
- Update the page's title to be consistent and show in site. by @kschaab in
|
||||
[#16174](https://github.com/google-gemini/gemini-cli/pull/16174)
|
||||
- docs: correct typo in bufferFastReturn JSDoc ("accomodate" → "accommodate") by
|
||||
@minglu7 in [#16056](https://github.com/google-gemini/gemini-cli/pull/16056)
|
||||
- fix: typo in MCP servers settings description by @alphanota in
|
||||
[#15929](https://github.com/google-gemini/gemini-cli/pull/15929)
|
||||
- fix: yolo should auto allow redirection by @abhipatel12 in
|
||||
[#16183](https://github.com/google-gemini/gemini-cli/pull/16183)
|
||||
- fix(cli): disableYoloMode shouldn't enforce default approval mode against args
|
||||
by @psinha40898 in
|
||||
[#16155](https://github.com/google-gemini/gemini-cli/pull/16155)
|
||||
- feat: add native Sublime Text support to IDE detection by @phreakocious in
|
||||
[#16083](https://github.com/google-gemini/gemini-cli/pull/16083)
|
||||
- refactor(core): extract ToolModificationHandler from scheduler by @abhipatel12
|
||||
in [#16118](https://github.com/google-gemini/gemini-cli/pull/16118)
|
||||
- Add support for Antigravity terminal in terminal setup utility by @raky291 in
|
||||
[#16051](https://github.com/google-gemini/gemini-cli/pull/16051)
|
||||
- feat(core): Wire up model routing to subagents. by @joshualitt in
|
||||
[#16043](https://github.com/google-gemini/gemini-cli/pull/16043)
|
||||
- feat(cli): add /agents slash command to list available agents by @adamfweidman
|
||||
in [#16182](https://github.com/google-gemini/gemini-cli/pull/16182)
|
||||
- docs(cli): fix includeDirectories nesting in configuration.md by @maru0804 in
|
||||
[#15067](https://github.com/google-gemini/gemini-cli/pull/15067)
|
||||
- feat: implement file system reversion utilities for rewind by @Adib234 in
|
||||
[#15715](https://github.com/google-gemini/gemini-cli/pull/15715)
|
||||
- Always enable redaction in GitHub actions. by @gundermanc in
|
||||
[#16200](https://github.com/google-gemini/gemini-cli/pull/16200)
|
||||
- fix: remove unsupported 'enabled' key from workflow config by @Han5991 in
|
||||
[#15611](https://github.com/google-gemini/gemini-cli/pull/15611)
|
||||
- docs: Remove redundant and duplicate documentation files by @liqzheng in
|
||||
[#14699](https://github.com/google-gemini/gemini-cli/pull/14699)
|
||||
- docs: shorten run command and use published version by @dsherret in
|
||||
[#16172](https://github.com/google-gemini/gemini-cli/pull/16172)
|
||||
- test(command-registry): increase initialization test timeout by @wszqkzqk in
|
||||
[#15979](https://github.com/google-gemini/gemini-cli/pull/15979)
|
||||
- Ensure TERM is set to xterm-256color by @falouu in
|
||||
[#15828](https://github.com/google-gemini/gemini-cli/pull/15828)
|
||||
- The telemetry.js script should handle paths that contain spaces by @JohnJAS in
|
||||
[#12078](https://github.com/google-gemini/gemini-cli/pull/12078)
|
||||
- ci: guard links workflow from running on forks by @wtanaka in
|
||||
[#15461](https://github.com/google-gemini/gemini-cli/pull/15461)
|
||||
- ci: guard nightly release workflow from running on forks by @wtanaka in
|
||||
[#15463](https://github.com/google-gemini/gemini-cli/pull/15463)
|
||||
- Support @ suggestions for subagenets by @sehoon38 in
|
||||
[#16201](https://github.com/google-gemini/gemini-cli/pull/16201)
|
||||
- feat(hooks): Support explicit stop and block execution control in model hooks
|
||||
by @SandyTao520 in
|
||||
[#15947](https://github.com/google-gemini/gemini-cli/pull/15947)
|
||||
- Refine Gemini 3 system instructions to reduce model verbosity by
|
||||
[#16549](https://github.com/google-gemini/gemini-cli/pull/16549)
|
||||
- fix: replace 3 consecutive periods with ellipsis character by @Vist233 in
|
||||
[#16587](https://github.com/google-gemini/gemini-cli/pull/16587)
|
||||
- chore(automation): ensure status/need-triage is applied and never cleared
|
||||
automatically by @bdmorgan in
|
||||
[#16657](https://github.com/google-gemini/gemini-cli/pull/16657)
|
||||
- fix: Handle colons in skill description frontmatter by @maru0804 in
|
||||
[#16345](https://github.com/google-gemini/gemini-cli/pull/16345)
|
||||
- refactor(core): harden skill frontmatter parsing by @NTaylorMullen in
|
||||
[#16705](https://github.com/google-gemini/gemini-cli/pull/16705)
|
||||
- feat(skills): add conflict detection and warnings for skill overrides by
|
||||
@NTaylorMullen in
|
||||
[#16139](https://github.com/google-gemini/gemini-cli/pull/16139)
|
||||
- chore: clean up unused models and use consts by @sehoon38 in
|
||||
[#16246](https://github.com/google-gemini/gemini-cli/pull/16246)
|
||||
- Always enable bracketed paste by @scidomino in
|
||||
[#16179](https://github.com/google-gemini/gemini-cli/pull/16179)
|
||||
- refactor: migrate clearCommand hook calls to HookSystem by @ved015 in
|
||||
[#16157](https://github.com/google-gemini/gemini-cli/pull/16157)
|
||||
- refactor: migrate app containter hook calls to hook system by @ishaanxgupta in
|
||||
[#16161](https://github.com/google-gemini/gemini-cli/pull/16161)
|
||||
- Show settings source in extensions lists by @chrstnb in
|
||||
[#16207](https://github.com/google-gemini/gemini-cli/pull/16207)
|
||||
- feat(skills): add pr-creator skill and enable skills by @NTaylorMullen in
|
||||
[#16232](https://github.com/google-gemini/gemini-cli/pull/16232)
|
||||
- fix: handle Shift+Space in Kitty keyboard protocol terminals by @tt-a1i in
|
||||
[#15767](https://github.com/google-gemini/gemini-cli/pull/15767)
|
||||
- feat(core, ui): Add /agents refresh command. by @joshualitt in
|
||||
[#16204](https://github.com/google-gemini/gemini-cli/pull/16204)
|
||||
- feat(core): add local experiments override via GEMINI_EXP by @kevin-ramdass in
|
||||
[#16181](https://github.com/google-gemini/gemini-cli/pull/16181)
|
||||
- feat(ui): reduce home directory warning noise and add opt-out setting by
|
||||
[#16709](https://github.com/google-gemini/gemini-cli/pull/16709)
|
||||
- feat(scheduler): add SchedulerStateManager for reactive tool state by
|
||||
@abhipatel12 in
|
||||
[#16651](https://github.com/google-gemini/gemini-cli/pull/16651)
|
||||
- chore(automation): enforce 'help wanted' label permissions and update
|
||||
guidelines by @bdmorgan in
|
||||
[#16707](https://github.com/google-gemini/gemini-cli/pull/16707)
|
||||
- fix(core): resolve circular dependency via tsconfig paths by @sehoon38 in
|
||||
[#16730](https://github.com/google-gemini/gemini-cli/pull/16730)
|
||||
- chore/release: bump version to 0.26.0-nightly.20260115.6cb3ae4e0 by
|
||||
@gemini-cli-robot in
|
||||
[#16738](https://github.com/google-gemini/gemini-cli/pull/16738)
|
||||
- fix(automation): correct status/need-issue label matching wildcard by
|
||||
@bdmorgan in [#16727](https://github.com/google-gemini/gemini-cli/pull/16727)
|
||||
- fix(automation): prevent label-enforcer loop by ignoring all bots by @bdmorgan
|
||||
in [#16746](https://github.com/google-gemini/gemini-cli/pull/16746)
|
||||
- Add links to supported locations and minor fixes by @g-samroberts in
|
||||
[#16476](https://github.com/google-gemini/gemini-cli/pull/16476)
|
||||
- feat(policy): add source tracking to policy rules by @allenhutchison in
|
||||
[#16670](https://github.com/google-gemini/gemini-cli/pull/16670)
|
||||
- feat(automation): enforce '🔒 maintainer only' and fix bot loop by @bdmorgan
|
||||
in [#16751](https://github.com/google-gemini/gemini-cli/pull/16751)
|
||||
- Make merged settings non-nullable and fix all lints related to that. by
|
||||
@jacob314 in [#16647](https://github.com/google-gemini/gemini-cli/pull/16647)
|
||||
- fix(core): prevent ModelInfo event emission on aborted signal by @sehoon38 in
|
||||
[#16752](https://github.com/google-gemini/gemini-cli/pull/16752)
|
||||
- Replace relative paths to fix website build by @chrstnb in
|
||||
[#16755](https://github.com/google-gemini/gemini-cli/pull/16755)
|
||||
- Restricting to localhost by @cocosheng-g in
|
||||
[#16548](https://github.com/google-gemini/gemini-cli/pull/16548)
|
||||
- fix(cli): add explicit dependency on color-convert by @sehoon38 in
|
||||
[#16757](https://github.com/google-gemini/gemini-cli/pull/16757)
|
||||
- fix(automation): robust label enforcement with permission checks by @bdmorgan
|
||||
in [#16762](https://github.com/google-gemini/gemini-cli/pull/16762)
|
||||
- fix(cli): prevent OOM crash by limiting file search traversal and adding
|
||||
timeout by @galz10 in
|
||||
[#16696](https://github.com/google-gemini/gemini-cli/pull/16696)
|
||||
- fix(cli): safely handle /dev/tty access on macOS by @korade-krushna in
|
||||
[#16531](https://github.com/google-gemini/gemini-cli/pull/16531)
|
||||
- docs: clarify workspace test execution in GEMINI.md by @mattKorwel in
|
||||
[#16764](https://github.com/google-gemini/gemini-cli/pull/16764)
|
||||
- Add support for running available commands prior to MCP servers loading by
|
||||
@Adib234 in [#15596](https://github.com/google-gemini/gemini-cli/pull/15596)
|
||||
- feat(plan): add experimental 'plan' approval mode by @jerop in
|
||||
[#16753](https://github.com/google-gemini/gemini-cli/pull/16753)
|
||||
- feat(scheduler): add functional awaitConfirmation utility by @abhipatel12 in
|
||||
[#16721](https://github.com/google-gemini/gemini-cli/pull/16721)
|
||||
- fix(infra): update maintainer rollup label to 'workstream-rollup' by @bdmorgan
|
||||
in [#16809](https://github.com/google-gemini/gemini-cli/pull/16809)
|
||||
- fix(infra): use GraphQL to detect direct parents in rollup workflow by
|
||||
@bdmorgan in [#16811](https://github.com/google-gemini/gemini-cli/pull/16811)
|
||||
- chore(workflows): rename label-workstream-rollup workflow by @bdmorgan in
|
||||
[#16818](https://github.com/google-gemini/gemini-cli/pull/16818)
|
||||
- skip simple-mcp-server.test.ts by @scidomino in
|
||||
[#16842](https://github.com/google-gemini/gemini-cli/pull/16842)
|
||||
- Steer outer agent to use expert subagents when present by @gundermanc in
|
||||
[#16763](https://github.com/google-gemini/gemini-cli/pull/16763)
|
||||
- Fix race condition by awaiting scheduleToolCalls by @chrstnb in
|
||||
[#16759](https://github.com/google-gemini/gemini-cli/pull/16759)
|
||||
- cleanup: Organize key bindings by @scidomino in
|
||||
[#16798](https://github.com/google-gemini/gemini-cli/pull/16798)
|
||||
- feat(core): Add generalist agent. by @joshualitt in
|
||||
[#16638](https://github.com/google-gemini/gemini-cli/pull/16638)
|
||||
- perf(ui): optimize text buffer and highlighting for large inputs by
|
||||
@NTaylorMullen in
|
||||
[#16229](https://github.com/google-gemini/gemini-cli/pull/16229)
|
||||
- refactor: migrate chatCompressionService to use HookSystem by @ved015 in
|
||||
[#16259](https://github.com/google-gemini/gemini-cli/pull/16259)
|
||||
- fix: properly use systemMessage for hooks in UI by @jackwotherspoon in
|
||||
[#16250](https://github.com/google-gemini/gemini-cli/pull/16250)
|
||||
- Infer modifyOtherKeys support by @scidomino in
|
||||
[#16270](https://github.com/google-gemini/gemini-cli/pull/16270)
|
||||
- feat(core): Cache ignore instances for performance by @EricRahm in
|
||||
[#16185](https://github.com/google-gemini/gemini-cli/pull/16185)
|
||||
- feat: apply remote admin settings (no-op) by @skeshive in
|
||||
[#16106](https://github.com/google-gemini/gemini-cli/pull/16106)
|
||||
- Autogenerate docs/cli/settings.md docs/getting-started/configuration.md was
|
||||
already autogenerated but settings.md was not. by @jacob314 in
|
||||
[#14408](https://github.com/google-gemini/gemini-cli/pull/14408)
|
||||
- refactor(config): remove legacy V1 settings migration logic by @galz10 in
|
||||
[#16252](https://github.com/google-gemini/gemini-cli/pull/16252)
|
||||
- Fix an issue where the agent stops prematurely by @gundermanc in
|
||||
[#16269](https://github.com/google-gemini/gemini-cli/pull/16269)
|
||||
- Update system prompt to prefer non-interactive commands by @NTaylorMullen in
|
||||
[#16117](https://github.com/google-gemini/gemini-cli/pull/16117)
|
||||
- Update ink version to 6.4.7 by @jacob314 in
|
||||
[#16284](https://github.com/google-gemini/gemini-cli/pull/16284)
|
||||
- Support for Built-in Agent Skills by @NTaylorMullen in
|
||||
[#16045](https://github.com/google-gemini/gemini-cli/pull/16045)
|
||||
- fix(skills): remove "Restart required" message from non-interactive commands
|
||||
by @NTaylorMullen in
|
||||
[#16307](https://github.com/google-gemini/gemini-cli/pull/16307)
|
||||
- remove unused sessionHookTriggers and exports by @ved015 in
|
||||
[#16324](https://github.com/google-gemini/gemini-cli/pull/16324)
|
||||
- Triage action cleanup by @bdmorgan in
|
||||
[#16319](https://github.com/google-gemini/gemini-cli/pull/16319)
|
||||
- fix: Add event-driven trigger to issue triage workflow by @bdmorgan in
|
||||
[#16334](https://github.com/google-gemini/gemini-cli/pull/16334)
|
||||
- fix(workflows): resolve triage workflow failures and actionlint errors by
|
||||
@bdmorgan in [#16338](https://github.com/google-gemini/gemini-cli/pull/16338)
|
||||
- docs: add note about experimental hooks by @abhipatel12 in
|
||||
[#16337](https://github.com/google-gemini/gemini-cli/pull/16337)
|
||||
- feat(cli): implement passive activity logger for session analysis by
|
||||
[#16782](https://github.com/google-gemini/gemini-cli/pull/16782)
|
||||
- fix(core): fix PTY descriptor shell leak by @galz10 in
|
||||
[#16773](https://github.com/google-gemini/gemini-cli/pull/16773)
|
||||
- feat(plan): enforce strict read-only policy and halt execution on violation by
|
||||
@jerop in [#16849](https://github.com/google-gemini/gemini-cli/pull/16849)
|
||||
- remove need-triage label from bug_report template by @sehoon38 in
|
||||
[#16864](https://github.com/google-gemini/gemini-cli/pull/16864)
|
||||
- fix(core): truncate large telemetry log entries by @sehoon38 in
|
||||
[#16769](https://github.com/google-gemini/gemini-cli/pull/16769)
|
||||
- docs(extensions): add Agent Skills support and mark feature as experimental by
|
||||
@NTaylorMullen in
|
||||
[#16859](https://github.com/google-gemini/gemini-cli/pull/16859)
|
||||
- fix(core): surface warnings for invalid hook event names in configuration
|
||||
(#16788) by @sehoon38 in
|
||||
[#16873](https://github.com/google-gemini/gemini-cli/pull/16873)
|
||||
- feat(plan): remove read_many_files from approval mode policies by @jerop in
|
||||
[#16876](https://github.com/google-gemini/gemini-cli/pull/16876)
|
||||
- feat(admin): implement admin controls polling and restart prompt by @skeshive
|
||||
in [#16627](https://github.com/google-gemini/gemini-cli/pull/16627)
|
||||
- Remove LRUCache class migrating to mnemoist by @jacob314 in
|
||||
[#16872](https://github.com/google-gemini/gemini-cli/pull/16872)
|
||||
- feat(settings): rename negative settings to positive naming (disable* ->
|
||||
enable*) by @afarber in
|
||||
[#14142](https://github.com/google-gemini/gemini-cli/pull/14142)
|
||||
- refactor(cli): unify shell confirmation dialogs by @NTaylorMullen in
|
||||
[#16828](https://github.com/google-gemini/gemini-cli/pull/16828)
|
||||
- feat(agent): enable agent skills by default by @NTaylorMullen in
|
||||
[#16736](https://github.com/google-gemini/gemini-cli/pull/16736)
|
||||
- refactor(core): foundational truncation refactoring and token estimation
|
||||
optimization by @NTaylorMullen in
|
||||
[#16824](https://github.com/google-gemini/gemini-cli/pull/16824)
|
||||
- fix(hooks): enable /hooks disable to reliably stop single hooks by
|
||||
@abhipatel12 in
|
||||
[#16804](https://github.com/google-gemini/gemini-cli/pull/16804)
|
||||
- Don't commit unless user asks us to. by @gundermanc in
|
||||
[#16902](https://github.com/google-gemini/gemini-cli/pull/16902)
|
||||
- chore: remove a2a-adapter and bump @a2a-js/sdk to 0.3.8 by @adamfweidman in
|
||||
[#16800](https://github.com/google-gemini/gemini-cli/pull/16800)
|
||||
- fix: Show experiment values in settings UI for compressionThreshold by
|
||||
@ishaanxgupta in
|
||||
[#16267](https://github.com/google-gemini/gemini-cli/pull/16267)
|
||||
- feat(cli): replace relative keyboard shortcuts link with web URL by
|
||||
@imaliabbas in
|
||||
[#16479](https://github.com/google-gemini/gemini-cli/pull/16479)
|
||||
- fix(core): resolve PKCE length issue and stabilize OAuth redirect port by
|
||||
@sehoon38 in [#16815](https://github.com/google-gemini/gemini-cli/pull/16815)
|
||||
- Delete rewind documentation for now by @Adib234 in
|
||||
[#16932](https://github.com/google-gemini/gemini-cli/pull/16932)
|
||||
- Stabilize skill-creator CI and package format by @NTaylorMullen in
|
||||
[#17001](https://github.com/google-gemini/gemini-cli/pull/17001)
|
||||
- Stabilize the git evals by @gundermanc in
|
||||
[#16989](https://github.com/google-gemini/gemini-cli/pull/16989)
|
||||
- fix(core): attempt compression before context overflow check by @NTaylorMullen
|
||||
in [#16914](https://github.com/google-gemini/gemini-cli/pull/16914)
|
||||
- Fix inverted logic. by @gundermanc in
|
||||
[#17007](https://github.com/google-gemini/gemini-cli/pull/17007)
|
||||
- chore(scripts): add duplicate issue closer script and fix lint errors by
|
||||
@bdmorgan in [#16997](https://github.com/google-gemini/gemini-cli/pull/16997)
|
||||
- docs: update README and config guide to reference Gemini 3 by @JayadityaGit in
|
||||
[#15806](https://github.com/google-gemini/gemini-cli/pull/15806)
|
||||
- fix(cli): correct Homebrew installation detection by @kij in
|
||||
[#14727](https://github.com/google-gemini/gemini-cli/pull/14727)
|
||||
- Demote git evals to nightly run. by @gundermanc in
|
||||
[#17030](https://github.com/google-gemini/gemini-cli/pull/17030)
|
||||
- fix(cli): use OSC-52 clipboard copy in Windows Terminal by @Thomas-Shephard in
|
||||
[#16920](https://github.com/google-gemini/gemini-cli/pull/16920)
|
||||
- Fix: Process all parts in response chunks when thought is first by @pyrytakala
|
||||
in [#13539](https://github.com/google-gemini/gemini-cli/pull/13539)
|
||||
- fix(automation): fix jq quoting error in pr-triage.sh by @Kimsoo0119 in
|
||||
[#16958](https://github.com/google-gemini/gemini-cli/pull/16958)
|
||||
- refactor(core): decouple scheduler into orchestration, policy, and
|
||||
confirmation by @abhipatel12 in
|
||||
[#16895](https://github.com/google-gemini/gemini-cli/pull/16895)
|
||||
- feat: add /introspect slash command by @NTaylorMullen in
|
||||
[#17048](https://github.com/google-gemini/gemini-cli/pull/17048)
|
||||
- refactor(cli): centralize tool mapping and decouple legacy scheduler by
|
||||
@abhipatel12 in
|
||||
[#17044](https://github.com/google-gemini/gemini-cli/pull/17044)
|
||||
- fix(ui): ensure rationale renders before tool calls by @NTaylorMullen in
|
||||
[#17043](https://github.com/google-gemini/gemini-cli/pull/17043)
|
||||
- fix(workflows): use author_association for maintainer check by @bdmorgan in
|
||||
[#17060](https://github.com/google-gemini/gemini-cli/pull/17060)
|
||||
- fix return type of fireSessionStartEvent to defaultHookOutput by @ved015 in
|
||||
[#16833](https://github.com/google-gemini/gemini-cli/pull/16833)
|
||||
- feat(cli): add experiment gate for event-driven scheduler by @abhipatel12 in
|
||||
[#17055](https://github.com/google-gemini/gemini-cli/pull/17055)
|
||||
- feat(core): improve shell redirection transparency and security by
|
||||
@NTaylorMullen in
|
||||
[#16486](https://github.com/google-gemini/gemini-cli/pull/16486)
|
||||
- fix(core): deduplicate ModelInfo emission in GeminiClient by @NTaylorMullen in
|
||||
[#17075](https://github.com/google-gemini/gemini-cli/pull/17075)
|
||||
- docs(themes): remove unsupported DiffModified color key by @jw409 in
|
||||
[#17073](https://github.com/google-gemini/gemini-cli/pull/17073)
|
||||
- fix: update currentSequenceModel when modelChanged by @adamfweidman in
|
||||
[#17051](https://github.com/google-gemini/gemini-cli/pull/17051)
|
||||
- feat(core): enhanced anchored iterative context compression with
|
||||
self-verification by @rmedranollamas in
|
||||
[#15710](https://github.com/google-gemini/gemini-cli/pull/15710)
|
||||
- Fix mcp instructions by @chrstnb in
|
||||
[#16439](https://github.com/google-gemini/gemini-cli/pull/16439)
|
||||
- [A2A] Disable checkpointing if git is not installed by @cocosheng-g in
|
||||
[#16896](https://github.com/google-gemini/gemini-cli/pull/16896)
|
||||
- feat(admin): set admin.skills.enabled based on advancedFeaturesEnabled setting
|
||||
by @skeshive in
|
||||
[#17095](https://github.com/google-gemini/gemini-cli/pull/17095)
|
||||
- Test coverage for hook exit code cases by @gundermanc in
|
||||
[#17041](https://github.com/google-gemini/gemini-cli/pull/17041)
|
||||
- Revert "Revert "Update extension examples"" by @chrstnb in
|
||||
[#16445](https://github.com/google-gemini/gemini-cli/pull/16445)
|
||||
- fix(core): Provide compact, actionable errors for agent delegation failures by
|
||||
@SandyTao520 in
|
||||
[#15829](https://github.com/google-gemini/gemini-cli/pull/15829)
|
||||
- feat(cli): add /chat debug command for nightly builds by @abhipatel12 in
|
||||
[#16339](https://github.com/google-gemini/gemini-cli/pull/16339)
|
||||
- style: format pr-creator skill by @NTaylorMullen in
|
||||
[#16381](https://github.com/google-gemini/gemini-cli/pull/16381)
|
||||
- feat(cli): Hooks enable-all/disable-all feature with dynamic status by
|
||||
@AbdulTawabJuly in
|
||||
[#15552](https://github.com/google-gemini/gemini-cli/pull/15552)
|
||||
- fix(core): ensure silent local subagent delegation while allowing remote
|
||||
confirmation by @adamfweidman in
|
||||
[#16395](https://github.com/google-gemini/gemini-cli/pull/16395)
|
||||
- Markdown w/ Frontmatter Agent Parser by @sehoon38 in
|
||||
[#16094](https://github.com/google-gemini/gemini-cli/pull/16094)
|
||||
- Fix crash on unicode character by @chrstnb in
|
||||
[#16420](https://github.com/google-gemini/gemini-cli/pull/16420)
|
||||
- Attempt to resolve OOM w/ useMemo on history items by @chrstnb in
|
||||
[#16424](https://github.com/google-gemini/gemini-cli/pull/16424)
|
||||
- fix(core): ensure sub-agent schema and prompt refresh during runtime by
|
||||
@adamfweidman in
|
||||
[#16409](https://github.com/google-gemini/gemini-cli/pull/16409)
|
||||
- Update extension examples by @chrstnb in
|
||||
[#16274](https://github.com/google-gemini/gemini-cli/pull/16274)
|
||||
- revert the change that was recently added from a fix by @sehoon38 in
|
||||
[#16390](https://github.com/google-gemini/gemini-cli/pull/16390)
|
||||
- Add other hook wrapper methods to hooksystem by @ved015 in
|
||||
[#16361](https://github.com/google-gemini/gemini-cli/pull/16361)
|
||||
- feat: introduce useRewindLogic hook for conversation history navigation by
|
||||
@Adib234 in [#15716](https://github.com/google-gemini/gemini-cli/pull/15716)
|
||||
- docs: Fix formatting issue in memport documentation by @wanglc02 in
|
||||
[#14774](https://github.com/google-gemini/gemini-cli/pull/14774)
|
||||
- fix(policy): enhance shell command safety and parsing by @allenhutchison in
|
||||
[#15034](https://github.com/google-gemini/gemini-cli/pull/15034)
|
||||
- fix(core): avoid 'activate_skill' re-registration warning by @NTaylorMullen in
|
||||
[#16398](https://github.com/google-gemini/gemini-cli/pull/16398)
|
||||
- perf(workflows): optimize PR triage script for faster execution by @bdmorgan
|
||||
in [#16355](https://github.com/google-gemini/gemini-cli/pull/16355)
|
||||
- feat(admin): prompt user to restart the CLI if they change auth to oauth
|
||||
mid-session or don't have auth type selected at start of session by @skeshive
|
||||
in [#16426](https://github.com/google-gemini/gemini-cli/pull/16426)
|
||||
- Update cli-help agent's system prompt in sub-agents section by @sehoon38 in
|
||||
[#16441](https://github.com/google-gemini/gemini-cli/pull/16441)
|
||||
- Revert "Update extension examples" by @chrstnb in
|
||||
[#16442](https://github.com/google-gemini/gemini-cli/pull/16442)
|
||||
- Fix: add back fastreturn support by @scidomino in
|
||||
[#16440](https://github.com/google-gemini/gemini-cli/pull/16440)
|
||||
- feat(a2a): Introduce /memory command for a2a server by @cocosheng-g in
|
||||
[#14456](https://github.com/google-gemini/gemini-cli/pull/14456)
|
||||
- docs: fix broken internal link by using relative path by @Gong-Mi in
|
||||
[#15371](https://github.com/google-gemini/gemini-cli/pull/15371)
|
||||
- migrate yolo/auto-edit keybindings by @scidomino in
|
||||
[#16457](https://github.com/google-gemini/gemini-cli/pull/16457)
|
||||
- feat(cli): add install and uninstall commands for skills by @NTaylorMullen in
|
||||
[#16377](https://github.com/google-gemini/gemini-cli/pull/16377)
|
||||
- feat(ui): use Tab to switch focus between shell and input by @jacob314 in
|
||||
[#14332](https://github.com/google-gemini/gemini-cli/pull/14332)
|
||||
- feat(core): support shipping built-in skills with the CLI by @NTaylorMullen in
|
||||
[#16300](https://github.com/google-gemini/gemini-cli/pull/16300)
|
||||
- Collect hardware details telemetry. by @gundermanc in
|
||||
[#16119](https://github.com/google-gemini/gemini-cli/pull/16119)
|
||||
- feat(agents): improve UI feedback and parser reliability by @NTaylorMullen in
|
||||
[#16459](https://github.com/google-gemini/gemini-cli/pull/16459)
|
||||
- Migrate keybindings by @scidomino in
|
||||
[#16460](https://github.com/google-gemini/gemini-cli/pull/16460)
|
||||
- feat(cli): cleanup activity logs alongside session files by @SandyTao520 in
|
||||
[#16399](https://github.com/google-gemini/gemini-cli/pull/16399)
|
||||
- feat(cli): implement dynamic terminal tab titles for CLI status by
|
||||
@NTaylorMullen in
|
||||
[#16378](https://github.com/google-gemini/gemini-cli/pull/16378)
|
||||
- feat(core): add disableLLMCorrection setting to skip auto-correction in edit
|
||||
tools by @SandyTao520 in
|
||||
[#16000](https://github.com/google-gemini/gemini-cli/pull/16000)
|
||||
- fix: Set both tab and window title instead of just window title by
|
||||
@NTaylorMullen in
|
||||
[#16464](https://github.com/google-gemini/gemini-cli/pull/16464)
|
||||
- fix(policy): ensure MCP policies match unqualified names in non-interactive
|
||||
mode by @NTaylorMullen in
|
||||
[#16490](https://github.com/google-gemini/gemini-cli/pull/16490)
|
||||
- fix(cli): refine 'Action Required' indicator and focus hints by @NTaylorMullen
|
||||
in [#16497](https://github.com/google-gemini/gemini-cli/pull/16497)
|
||||
- Refactor beforeAgent and afterAgent hookEvents to follow desired output by
|
||||
@ved015 in [#16495](https://github.com/google-gemini/gemini-cli/pull/16495)
|
||||
- feat(agents): clarify mandatory YAML frontmatter for sub-agents by
|
||||
@NTaylorMullen in
|
||||
[#16515](https://github.com/google-gemini/gemini-cli/pull/16515)
|
||||
- docs(telemetry): add Google Cloud Monitoring dashboard documentation by @jerop
|
||||
in [#16520](https://github.com/google-gemini/gemini-cli/pull/16520)
|
||||
- Implement support for subagents as extensions. by @gundermanc in
|
||||
[#16473](https://github.com/google-gemini/gemini-cli/pull/16473)
|
||||
- refactor: make baseTimestamp optional in addItem and remove redundant calls by
|
||||
@sehoon38 in [#16471](https://github.com/google-gemini/gemini-cli/pull/16471)
|
||||
- Improve key binding names and descriptions by @scidomino in
|
||||
[#16529](https://github.com/google-gemini/gemini-cli/pull/16529)
|
||||
- feat(core, cli): Add support for agents in settings.json. by @joshualitt in
|
||||
[#16433](https://github.com/google-gemini/gemini-cli/pull/16433)
|
||||
- fix(cli): fix 'gemini skills install' unknown argument error by @NTaylorMullen
|
||||
in [#16537](https://github.com/google-gemini/gemini-cli/pull/16537)
|
||||
- chore(ui): optimize AgentsStatus layout with dense list style and group
|
||||
separation by @adamfweidman in
|
||||
[#16545](https://github.com/google-gemini/gemini-cli/pull/16545)
|
||||
- fix(cli): allow @ file selector on slash command lines by @galz10 in
|
||||
[#16370](https://github.com/google-gemini/gemini-cli/pull/16370)
|
||||
- fix(ui): resolve sticky header regression in tool messages by @jacob314 in
|
||||
[#16514](https://github.com/google-gemini/gemini-cli/pull/16514)
|
||||
- feat(core): Align internal agent settings with configs exposed through
|
||||
settings.json by @joshualitt in
|
||||
[#16458](https://github.com/google-gemini/gemini-cli/pull/16458)
|
||||
- fix(cli): copy uses OSC52 only in SSH/WSL by @assagman in
|
||||
[#16554](https://github.com/google-gemini/gemini-cli/pull/16554)
|
||||
- docs(skills): clarify skill directory structure and file location by
|
||||
@NTaylorMullen in
|
||||
[#16532](https://github.com/google-gemini/gemini-cli/pull/16532)
|
||||
- Fix: make ctrl+x use preferred editor by @scidomino in
|
||||
[#16556](https://github.com/google-gemini/gemini-cli/pull/16556)
|
||||
- fix(core): Resolve race condition in tool response reporting by @abhipatel12
|
||||
in [#16557](https://github.com/google-gemini/gemini-cli/pull/16557)
|
||||
- feat(ui): highlight persist mode status in ModelDialog by @sehoon38 in
|
||||
[#16483](https://github.com/google-gemini/gemini-cli/pull/16483)
|
||||
- refactor: clean up A2A task output for users and LLMs by @adamfweidman in
|
||||
[#16561](https://github.com/google-gemini/gemini-cli/pull/16561)
|
||||
- feat(core/ui): enhance retry mechanism and UX by @sehoon38 in
|
||||
[#16489](https://github.com/google-gemini/gemini-cli/pull/16489)
|
||||
- Modernize MaxSizedBox to use and ResizeObservers by @jacob314 in
|
||||
[#16565](https://github.com/google-gemini/gemini-cli/pull/16565)
|
||||
- Behavioral evals framework. by @gundermanc in
|
||||
[#16047](https://github.com/google-gemini/gemini-cli/pull/16047)
|
||||
- Aggregate test results. by @gundermanc in
|
||||
[#16581](https://github.com/google-gemini/gemini-cli/pull/16581)
|
||||
- feat(admin): support admin-enforced settings for Agent Skills by
|
||||
@NTaylorMullen in
|
||||
[#16406](https://github.com/google-gemini/gemini-cli/pull/16406)
|
||||
- fix(patch): cherry-pick cfdc4cf to release/v0.25.0-preview.0-pr-16759 to patch
|
||||
version v0.25.0-preview.0 and create version 0.25.0-preview.1 by
|
||||
@gemini-cli-robot in
|
||||
[#16866](https://github.com/google-gemini/gemini-cli/pull/16866)
|
||||
- Patch #16730 into v0.25.0 preview by @chrstnb in
|
||||
[#16882](https://github.com/google-gemini/gemini-cli/pull/16882)
|
||||
- fix(patch): cherry-pick 3b55581 to release/v0.25.0-preview.2-pr-16506 to patch
|
||||
version v0.25.0-preview.2 and create version 0.25.0-preview.3 by
|
||||
@gemini-cli-robot in
|
||||
[#17098](https://github.com/google-gemini/gemini-cli/pull/17098)
|
||||
[#16493](https://github.com/google-gemini/gemini-cli/pull/16493)
|
||||
- fix: migrate BeforeModel and AfterModel hooks to HookSystem by @ved015 in
|
||||
[#16599](https://github.com/google-gemini/gemini-cli/pull/16599)
|
||||
- feat(admin): apply admin settings to gemini skills/mcp/extensions commands by
|
||||
@skeshive in [#17102](https://github.com/google-gemini/gemini-cli/pull/17102)
|
||||
- fix(core): update telemetry token count after session resume by @psinha40898
|
||||
in [#15491](https://github.com/google-gemini/gemini-cli/pull/15491)
|
||||
- Demote the subagent test to nightly by @gundermanc in
|
||||
[#17105](https://github.com/google-gemini/gemini-cli/pull/17105)
|
||||
- feat(plan): telemetry to track adoption and usage of plan mode by @Adib234 in
|
||||
[#16863](https://github.com/google-gemini/gemini-cli/pull/16863)
|
||||
- feat: Add flash lite utility fallback chain by @adamfweidman in
|
||||
[#17056](https://github.com/google-gemini/gemini-cli/pull/17056)
|
||||
- Fixes Windows crash: "Cannot resize a pty that has already exited" by @dzammit
|
||||
in [#15757](https://github.com/google-gemini/gemini-cli/pull/15757)
|
||||
- feat(core): Add initial eval for generalist agent. by @joshualitt in
|
||||
[#16856](https://github.com/google-gemini/gemini-cli/pull/16856)
|
||||
- feat(core): unify agent enabled and disabled flags by @SandyTao520 in
|
||||
[#17127](https://github.com/google-gemini/gemini-cli/pull/17127)
|
||||
- fix(core): resolve auto model in default strategy by @sehoon38 in
|
||||
[#17116](https://github.com/google-gemini/gemini-cli/pull/17116)
|
||||
- docs: update project context and pr-creator workflow by @NTaylorMullen in
|
||||
[#17119](https://github.com/google-gemini/gemini-cli/pull/17119)
|
||||
- fix(cli): send gemini-cli version as mcp client version by @dsp in
|
||||
[#13407](https://github.com/google-gemini/gemini-cli/pull/13407)
|
||||
- fix(cli): resolve Ctrl+Enter and Ctrl+J newline issues by @imadraude in
|
||||
[#17021](https://github.com/google-gemini/gemini-cli/pull/17021)
|
||||
- Remove missing sidebar item by @chrstnb in
|
||||
[#17145](https://github.com/google-gemini/gemini-cli/pull/17145)
|
||||
- feat(core): Ensure all properties in hooks object are event names. by
|
||||
@joshualitt in
|
||||
[#16870](https://github.com/google-gemini/gemini-cli/pull/16870)
|
||||
- fix(cli): fix newline support broken in previous PR by @scidomino in
|
||||
[#17159](https://github.com/google-gemini/gemini-cli/pull/17159)
|
||||
- Add interactive ValidationDialog for handling 403 VALIDATION_REQUIRED errors.
|
||||
by @gsquared94 in
|
||||
[#16231](https://github.com/google-gemini/gemini-cli/pull/16231)
|
||||
- Add Esc-Esc to clear prompt when it's not empty by @Adib234 in
|
||||
[#17131](https://github.com/google-gemini/gemini-cli/pull/17131)
|
||||
- Avoid spurious warnings about unexpected renders triggered by appEvents and
|
||||
coreEvents. by @jacob314 in
|
||||
[#17160](https://github.com/google-gemini/gemini-cli/pull/17160)
|
||||
- fix(cli): resolve home/end keybinding conflict by @scidomino in
|
||||
[#17124](https://github.com/google-gemini/gemini-cli/pull/17124)
|
||||
- fix(cli): display 'http' type on mcp list by @pamanta in
|
||||
[#16915](https://github.com/google-gemini/gemini-cli/pull/16915)
|
||||
- fix bad fallback logic external editor logic by @scidomino in
|
||||
[#17166](https://github.com/google-gemini/gemini-cli/pull/17166)
|
||||
- Fix bug where System scopes weren't migrated. by @jacob314 in
|
||||
[#17174](https://github.com/google-gemini/gemini-cli/pull/17174)
|
||||
- Fix mcp tool lookup in tool registry by @werdnum in
|
||||
[#17054](https://github.com/google-gemini/gemini-cli/pull/17054)
|
||||
|
||||
**Full changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.24.5...v0.25.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.25.2...v0.26.0
|
||||
|
||||
+415
-310
@@ -1,6 +1,6 @@
|
||||
# Preview release: Release v0.26.0-preview.0
|
||||
# Preview release: Release v0.27.0-preview.0
|
||||
|
||||
Released: January 21, 2026
|
||||
Released: January 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).
|
||||
@@ -13,320 +13,425 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Skills and Agents:** Improvements to the `activate_skill` tool and skill
|
||||
management. Experimental Agent Skills support.
|
||||
- **UI/UX:** Addition of a Rewind Confirmation dialog and Viewer component.
|
||||
- **Extensions:** Experimental setting for extension configuration.
|
||||
- **Bug Fixes and Stability:** PDF token estimation fix and improvements to
|
||||
scheduled issue triage.
|
||||
- **Event-Driven Architecture:** The tool execution scheduler is now
|
||||
event-driven, improving performance and reliability.
|
||||
- **System Prompt Override:** Now supports dynamic variable substitution.
|
||||
- **Rewind Command:** The `/rewind` command has been implemented.
|
||||
- **Linux Clipboard:** Image pasting capabilities for Wayland and X11 on Linux.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix: PDF token estimation
|
||||
([#16494](https://github.com/google-gemini/gemini-cli/pull/16494)) by
|
||||
@korade-krushna in
|
||||
[#16527](https://github.com/google-gemini/gemini-cli/pull/16527)
|
||||
- chore(release): bump version to 0.26.0-nightly.20260114.bb6c57414 by
|
||||
- remove fireAgent and beforeAgent hook by @ishaanxgupta in
|
||||
[#16919](https://github.com/google-gemini/gemini-cli/pull/16919)
|
||||
- Remove unused modelHooks and toolHooks by @ved015 in
|
||||
[#17115](https://github.com/google-gemini/gemini-cli/pull/17115)
|
||||
- feat(cli): sanitize ANSI escape sequences in non-interactive output by
|
||||
@sehoon38 in [#17172](https://github.com/google-gemini/gemini-cli/pull/17172)
|
||||
- Update Attempt text to Retry when showing the retry happening to the … by
|
||||
@sehoon38 in [#17178](https://github.com/google-gemini/gemini-cli/pull/17178)
|
||||
- chore(skills): update pr-creator skill workflow by @sehoon38 in
|
||||
[#17180](https://github.com/google-gemini/gemini-cli/pull/17180)
|
||||
- feat(cli): implement event-driven tool execution scheduler by @abhipatel12 in
|
||||
[#17078](https://github.com/google-gemini/gemini-cli/pull/17078)
|
||||
- chore(release): bump version to 0.27.0-nightly.20260121.97aac696f by
|
||||
@gemini-cli-robot in
|
||||
[#16604](https://github.com/google-gemini/gemini-cli/pull/16604)
|
||||
- docs: clarify F12 to open debug console by @jackwotherspoon in
|
||||
[#16570](https://github.com/google-gemini/gemini-cli/pull/16570)
|
||||
- docs: Remove .md extension from internal links in architecture.md by
|
||||
@medic-code in
|
||||
[#12899](https://github.com/google-gemini/gemini-cli/pull/12899)
|
||||
- Add an experimental setting for extension config by @chrstnb in
|
||||
[#16506](https://github.com/google-gemini/gemini-cli/pull/16506)
|
||||
- feat: add Rewind Confirmation dialog and Rewind Viewer component by @Adib234
|
||||
in [#15717](https://github.com/google-gemini/gemini-cli/pull/15717)
|
||||
- fix(a2a): Don't throw errors for GeminiEventType Retry and InvalidStream. by
|
||||
@ehedlund in [#16541](https://github.com/google-gemini/gemini-cli/pull/16541)
|
||||
- prefactor: add rootCommands as array so it can be used for policy parsing by
|
||||
[#17181](https://github.com/google-gemini/gemini-cli/pull/17181)
|
||||
- Remove other rewind reference in docs by @chrstnb in
|
||||
[#17149](https://github.com/google-gemini/gemini-cli/pull/17149)
|
||||
- feat(skills): add code-reviewer skill by @sehoon38 in
|
||||
[#17187](httpshttps://github.com/google-gemini/gemini-cli/pull/17187)
|
||||
- feat(plan): Extend Shift+Tab Mode Cycling to include Plan Mode by @Adib234 in
|
||||
[#17177](https://github.com/google-gemini/gemini-cli/pull/17177)
|
||||
- feat(plan): refactor TestRig and eval helper to support configurable approval
|
||||
modes by @jerop in
|
||||
[#17171](https://github.com/google-gemini/gemini-cli/pull/17171)
|
||||
- feat(workflows): support recursive workstream labeling and new IDs by
|
||||
@bdmorgan in [#17207](https://github.com/google-gemini/gemini-cli/pull/17207)
|
||||
- Run evals for all models. by @gundermanc in
|
||||
[#17123](https://github.com/google-gemini/gemini-cli/pull/17123)
|
||||
- fix(github): improve label-workstream-rollup efficiency with GraphQL by
|
||||
@bdmorgan in [#17217](https://github.com/google-gemini/gemini-cli/pull/17217)
|
||||
- Docs: Update changelogs for v.0.25.0 and v0.26.0-preview.0 releases. by
|
||||
@g-samroberts in
|
||||
[#17215](https://github.com/google-gemini/gemini-cli/pull/17215)
|
||||
- Migrate beforeTool and afterTool hooks to hookSystem by @ved015 in
|
||||
[#17204](https://github.com/google-gemini/gemini-cli/pull/17204)
|
||||
- fix(github): improve label-workstream-rollup efficiency and fix bugs by
|
||||
@bdmorgan in [#17219](https://github.com/google-gemini/gemini-cli/pull/17219)
|
||||
- feat(cli): improve skill enablement/disablement verbiage by @NTaylorMullen in
|
||||
[#17192](https://github.com/google-gemini/gemini-cli/pull/17192)
|
||||
- fix(admin): Ensure CLI commands run in non-interactive mode by @skeshive in
|
||||
[#17218](https://github.com/google-gemini/gemini-cli/pull/17218)
|
||||
- feat(core): support dynamic variable substitution in system prompt override by
|
||||
@NTaylorMullen in
|
||||
[#17042](https://github.com/google-gemini/gemini-cli/pull/17042)
|
||||
- fix(core,cli): enable recursive directory access for by @galz10 in
|
||||
[#17094](https://github.com/google-gemini/gemini-cli/pull/17094)
|
||||
- Docs: Marking for experimental features by @jkcinouye in
|
||||
[#16760](https://github.com/google-gemini/gemini-cli/pull/16760)
|
||||
- Support command/ctrl/alt backspace correctly by @scidomino in
|
||||
[#17175](https://github.com/google-gemini/gemini-cli/pull/17175)
|
||||
- feat(plan): add approval mode instructions to system prompt by @jerop in
|
||||
[#17151](https://github.com/google-gemini/gemini-cli/pull/17151)
|
||||
- feat(core): enable disableLLMCorrection by default by @SandyTao520 in
|
||||
[#17223](https://github.com/google-gemini/gemini-cli/pull/17223)
|
||||
- Remove unused slug from sidebar by @chrstnb in
|
||||
[#17229](https://github.com/google-gemini/gemini-cli/pull/17229)
|
||||
- drain stdin on exit by @scidomino in
|
||||
[#17241](https://github.com/google-gemini/gemini-cli/pull/17241)
|
||||
- refactor(cli): decouple UI from live tool execution via ToolActionsContext by
|
||||
@abhipatel12 in
|
||||
[#16640](https://github.com/google-gemini/gemini-cli/pull/16640)
|
||||
- remove unnecessary \x7f key bindings by @scidomino in
|
||||
[#16646](https://github.com/google-gemini/gemini-cli/pull/16646)
|
||||
- docs(skills): use body-file in pr-creator skill for better reliability by
|
||||
[#17183](https://github.com/google-gemini/gemini-cli/pull/17183)
|
||||
- fix(core): update token count and telemetry on /chat resume history load by
|
||||
@psinha40898 in
|
||||
[#16279](https://github.com/google-gemini/gemini-cli/pull/16279)
|
||||
- fix: /policy to display policies according to mode by @ishaanxgupta in
|
||||
[#16772](https://github.com/google-gemini/gemini-cli/pull/16772)
|
||||
- fix(core): simplify replace tool error message by @SandyTao520 in
|
||||
[#17246](https://github.com/google-gemini/gemini-cli/pull/17246)
|
||||
- feat(cli): consolidate shell inactivity and redirection monitoring by
|
||||
@NTaylorMullen in
|
||||
[#17086](https://github.com/google-gemini/gemini-cli/pull/17086)
|
||||
- fix(scheduler): prevent stale tool re-publication and fix stuck UI state by
|
||||
@abhipatel12 in
|
||||
[#16642](https://github.com/google-gemini/gemini-cli/pull/16642)
|
||||
- chore(automation): recursive labeling for workstream descendants by @bdmorgan
|
||||
in [#16609](https://github.com/google-gemini/gemini-cli/pull/16609)
|
||||
- feat: introduce 'skill-creator' built-in skill and CJS management tools by
|
||||
@NTaylorMullen in
|
||||
[#16394](https://github.com/google-gemini/gemini-cli/pull/16394)
|
||||
- chore(automation): remove automated PR size and complexity labeler by
|
||||
@bdmorgan in [#16648](https://github.com/google-gemini/gemini-cli/pull/16648)
|
||||
- refactor(skills): replace 'project' with 'workspace' scope by @NTaylorMullen
|
||||
in [#16380](https://github.com/google-gemini/gemini-cli/pull/16380)
|
||||
- Docs: Update release notes for 1/13/2026 by @jkcinouye in
|
||||
[#16583](https://github.com/google-gemini/gemini-cli/pull/16583)
|
||||
- Simplify paste handling by @scidomino in
|
||||
[#16654](https://github.com/google-gemini/gemini-cli/pull/16654)
|
||||
- chore(automation): improve scheduled issue triage discovery and throughput by
|
||||
@bdmorgan in [#16652](https://github.com/google-gemini/gemini-cli/pull/16652)
|
||||
- fix(acp): run exit cleanup when stdin closes by @codefromthecrypt in
|
||||
[#14953](https://github.com/google-gemini/gemini-cli/pull/14953)
|
||||
- feat(scheduler): add types needed for event driven scheduler by @abhipatel12
|
||||
in [#16641](https://github.com/google-gemini/gemini-cli/pull/16641)
|
||||
- Remove unused rewind key binding by @scidomino in
|
||||
[#16659](https://github.com/google-gemini/gemini-cli/pull/16659)
|
||||
- Remove sequence binding by @scidomino in
|
||||
[#16664](https://github.com/google-gemini/gemini-cli/pull/16664)
|
||||
- feat(cli): undeprecate the --prompt flag by @alexaustin007 in
|
||||
[#13981](https://github.com/google-gemini/gemini-cli/pull/13981)
|
||||
- chore: update dependabot configuration by @cosmopax in
|
||||
[#13507](https://github.com/google-gemini/gemini-cli/pull/13507)
|
||||
- feat(config): add 'auto' alias for default model selection by @sehoon38 in
|
||||
[#16661](https://github.com/google-gemini/gemini-cli/pull/16661)
|
||||
- Enable & disable agents by @sehoon38 in
|
||||
[#16225](https://github.com/google-gemini/gemini-cli/pull/16225)
|
||||
- cleanup: Improve keybindings by @scidomino in
|
||||
[#16672](https://github.com/google-gemini/gemini-cli/pull/16672)
|
||||
- Add timeout for shell-utils to prevent hangs. by @jacob314 in
|
||||
[#16667](https://github.com/google-gemini/gemini-cli/pull/16667)
|
||||
- feat(plan): add experimental plan flag by @jerop in
|
||||
[#16650](https://github.com/google-gemini/gemini-cli/pull/16650)
|
||||
- feat(cli): add security consent prompts for skill installation by
|
||||
@NTaylorMullen in
|
||||
[#16549](https://github.com/google-gemini/gemini-cli/pull/16549)
|
||||
- fix: replace 3 consecutive periods with ellipsis character by @Vist233 in
|
||||
[#16587](https://github.com/google-gemini/gemini-cli/pull/16587)
|
||||
- chore(automation): ensure status/need-triage is applied and never cleared
|
||||
automatically by @bdmorgan in
|
||||
[#16657](https://github.com/google-gemini/gemini-cli/pull/16657)
|
||||
- fix: Handle colons in skill description frontmatter by @maru0804 in
|
||||
[#16345](https://github.com/google-gemini/gemini-cli/pull/16345)
|
||||
- refactor(core): harden skill frontmatter parsing by @NTaylorMullen in
|
||||
[#16705](https://github.com/google-gemini/gemini-cli/pull/16705)
|
||||
- feat(skills): add conflict detection and warnings for skill overrides by
|
||||
@NTaylorMullen in
|
||||
[#16709](https://github.com/google-gemini/gemini-cli/pull/16709)
|
||||
- feat(scheduler): add SchedulerStateManager for reactive tool state by
|
||||
@abhipatel12 in
|
||||
[#16651](https://github.com/google-gemini/gemini-cli/pull/16651)
|
||||
- chore(automation): enforce 'help wanted' label permissions and update
|
||||
guidelines by @bdmorgan in
|
||||
[#16707](https://github.com/google-gemini/gemini-cli/pull/16707)
|
||||
- fix(core): resolve circular dependency via tsconfig paths by @sehoon38 in
|
||||
[#16730](https://github.com/google-gemini/gemini-cli/pull/16730)
|
||||
- chore/release: bump version to 0.26.0-nightly.20260115.6cb3ae4e0 by
|
||||
@gemini-cli-robot in
|
||||
[#16738](https://github.com/google-gemini/gemini-cli/pull/16738)
|
||||
- fix(automation): correct status/need-issue label matching wildcard by
|
||||
@bdmorgan in [#16727](https://github.com/google-gemini/gemini-cli/pull/16727)
|
||||
- fix(automation): prevent label-enforcer loop by ignoring all bots by @bdmorgan
|
||||
in [#16746](https://github.com/google-gemini/gemini-cli/pull/16746)
|
||||
- Add links to supported locations and minor fixes by @g-samroberts in
|
||||
[#16476](https://github.com/google-gemini/gemini-cli/pull/16476)
|
||||
- feat(policy): add source tracking to policy rules by @allenhutchison in
|
||||
[#16670](https://github.com/google-gemini/gemini-cli/pull/16670)
|
||||
- feat(automation): enforce '🔒 maintainer only' and fix bot loop by @bdmorgan
|
||||
in [#16751](https://github.com/google-gemini/gemini-cli/pull/16751)
|
||||
- Make merged settings non-nullable and fix all lints related to that. by
|
||||
@jacob314 in [#16647](https://github.com/google-gemini/gemini-cli/pull/16647)
|
||||
- fix(core): prevent ModelInfo event emission on aborted signal by @sehoon38 in
|
||||
[#16752](https://github.com/google-gemini/gemini-cli/pull/16752)
|
||||
- Replace relative paths to fix website build by @chrstnb in
|
||||
[#16755](https://github.com/google-gemini/gemini-cli/pull/16755)
|
||||
- Restricting to localhost by @cocosheng-g in
|
||||
[#16548](https://github.com/google-gemini/gemini-cli/pull/16548)
|
||||
- fix(cli): add explicit dependency on color-convert by @sehoon38 in
|
||||
[#16757](https://github.com/google-gemini/gemini-cli/pull/16757)
|
||||
- fix(automation): robust label enforcement with permission checks by @bdmorgan
|
||||
in [#16762](https://github.com/google-gemini/gemini-cli/pull/16762)
|
||||
- fix(cli): prevent OOM crash by limiting file search traversal and adding
|
||||
timeout by @galz10 in
|
||||
[#16696](https://github.com/google-gemini/gemini-cli/pull/16696)
|
||||
- fix(cli): safely handle /dev/tty access on macOS by @korade-krushna in
|
||||
[#16531](https://github.com/google-gemini/gemini-cli/pull/16531)
|
||||
- docs: clarify workspace test execution in GEMINI.md by @mattKorwel in
|
||||
[#16764](https://github.com/google-gemini/gemini-cli/pull/16764)
|
||||
- Add support for running available commands prior to MCP servers loading by
|
||||
@Adib234 in [#15596](https://github.com/google-gemini/gemini-cli/pull/15596)
|
||||
- feat(plan): add experimental 'plan' approval mode by @jerop in
|
||||
[#16753](https://github.com/google-gemini/gemini-cli/pull/16753)
|
||||
- feat(scheduler): add functional awaitConfirmation utility by @abhipatel12 in
|
||||
[#16721](https://github.com/google-gemini/gemini-cli/pull/16721)
|
||||
- fix(infra): update maintainer rollup label to 'workstream-rollup' by @bdmorgan
|
||||
in [#16809](https://github.com/google-gemini/gemini-cli/pull/16809)
|
||||
- fix(infra): use GraphQL to detect direct parents in rollup workflow by
|
||||
@bdmorgan in [#16811](https://github.com/google-gemini/gemini-cli/pull/16811)
|
||||
- chore(workflows): rename label-workstream-rollup workflow by @bdmorgan in
|
||||
[#16818](https://github.com/google-gemini/gemini-cli/pull/16818)
|
||||
- skip simple-mcp-server.test.ts by @scidomino in
|
||||
[#16842](https://github.com/google-gemini/gemini-cli/pull/16842)
|
||||
- Steer outer agent to use expert subagents when present by @gundermanc in
|
||||
[#16763](https://github.com/google-gemini/gemini-cli/pull/16763)
|
||||
- Fix race condition by awaiting scheduleToolCalls by @chrstnb in
|
||||
[#16759](https://github.com/google-gemini/gemini-cli/pull/16759)
|
||||
- cleanup: Organize key bindings by @scidomino in
|
||||
[#16798](https://github.com/google-gemini/gemini-cli/pull/16798)
|
||||
- feat(core): Add generalist agent. by @joshualitt in
|
||||
[#16638](https://github.com/google-gemini/gemini-cli/pull/16638)
|
||||
- perf(ui): optimize text buffer and highlighting for large inputs by
|
||||
@NTaylorMullen in
|
||||
[#16782](https://github.com/google-gemini/gemini-cli/pull/16782)
|
||||
- fix(core): fix PTY descriptor shell leak by @galz10 in
|
||||
[#16773](https://github.com/google-gemini/gemini-cli/pull/16773)
|
||||
- feat(plan): enforce strict read-only policy and halt execution on violation by
|
||||
@jerop in [#16849](https://github.com/google-gemini/gemini-cli/pull/16849)
|
||||
- remove need-triage label from bug_report template by @sehoon38 in
|
||||
[#16864](https://github.com/google-gemini/gemini-cli/pull/16864)
|
||||
- fix(core): truncate large telemetry log entries by @sehoon38 in
|
||||
[#16769](https://github.com/google-gemini/gemini-cli/pull/16769)
|
||||
- docs(extensions): add Agent Skills support and mark feature as experimental by
|
||||
@NTaylorMullen in
|
||||
[#16859](https://github.com/google-gemini/gemini-cli/pull/16859)
|
||||
- fix(core): surface warnings for invalid hook event names in configuration
|
||||
([#16788](https://github.com/google-gemini/gemini-cli/pull/16788)) by
|
||||
@sehoon38 in [#16873](https://github.com/google-gemini/gemini-cli/pull/16873)
|
||||
- feat(plan): remove read_many_files from approval mode policies by @jerop in
|
||||
[#16876](https://github.com/google-gemini/gemini-cli/pull/16876)
|
||||
- feat(admin): implement admin controls polling and restart prompt by @skeshive
|
||||
in [#16627](https://github.com/google-gemini/gemini-cli/pull/16627)
|
||||
- Remove LRUCache class migrating to mnemoist by @jacob314 in
|
||||
[#16872](https://github.com/google-gemini/gemini-cli/pull/16872)
|
||||
- feat(settings): rename negative settings to positive naming (disable* ->
|
||||
enable*) by @afarber in
|
||||
[#14142](https://github.com/google-gemini/gemini-cli/pull/14142)
|
||||
- refactor(cli): unify shell confirmation dialogs by @NTaylorMullen in
|
||||
[#16828](https://github.com/google-gemini/gemini-cli/pull/16828)
|
||||
- feat(agent): enable agent skills by default by @NTaylorMullen in
|
||||
[#16736](https://github.com/google-gemini/gemini-cli/pull/16736)
|
||||
- refactor(core): foundational truncation refactoring and token estimation
|
||||
optimization by @NTaylorMullen in
|
||||
[#16824](https://github.com/google-gemini/gemini-cli/pull/16824)
|
||||
- fix(hooks): enable /hooks disable to reliably stop single hooks by
|
||||
@abhipatel12 in
|
||||
[#16804](https://github.com/google-gemini/gemini-cli/pull/16804)
|
||||
- Don't commit unless user asks us to. by @gundermanc in
|
||||
[#16902](https://github.com/google-gemini/gemini-cli/pull/16902)
|
||||
- chore: remove a2a-adapter and bump @a2a-js/sdk to 0.3.8 by @adamfweidman in
|
||||
[#16800](https://github.com/google-gemini/gemini-cli/pull/16800)
|
||||
- fix: Show experiment values in settings UI for compressionThreshold by
|
||||
@ishaanxgupta in
|
||||
[#16267](https://github.com/google-gemini/gemini-cli/pull/16267)
|
||||
- feat(cli): replace relative keyboard shortcuts link with web URL by
|
||||
@imaliabbas in
|
||||
[#16479](https://github.com/google-gemini/gemini-cli/pull/16479)
|
||||
- fix(core): resolve PKCE length issue and stabilize OAuth redirect port by
|
||||
@sehoon38 in [#16815](https://github.com/google-gemini/gemini-cli/pull/16815)
|
||||
- Delete rewind documentation for now by @Adib234 in
|
||||
[#16932](https://github.com/google-gemini/gemini-cli/pull/16932)
|
||||
- Stabilize skill-creator CI and package format by @NTaylorMullen in
|
||||
[#17001](https://github.com/google-gemini/gemini-cli/pull/17001)
|
||||
- Stabilize the git evals by @gundermanc in
|
||||
[#16989](https://github.com/google-gemini/gemini-cli/pull/16989)
|
||||
- fix(core): attempt compression before context overflow check by @NTaylorMullen
|
||||
in [#16914](https://github.com/google-gemini/gemini-cli/pull/16914)
|
||||
- Fix inverted logic. by @gundermanc in
|
||||
[#17007](https://github.com/google-gemini/gemini-cli/pull/17007)
|
||||
- chore(scripts): add duplicate issue closer script and fix lint errors by
|
||||
@bdmorgan in [#16997](https://github.com/google-gemini/gemini-cli/pull/16997)
|
||||
- docs: update README and config guide to reference Gemini 3 by @JayadityaGit in
|
||||
[#15806](https://github.com/google-gemini/gemini-cli/pull/15806)
|
||||
- fix(cli): correct Homebrew installation detection by @kij in
|
||||
[#14727](https://github.com/google-gemini/gemini-cli/pull/14727)
|
||||
- Demote git evals to nightly run. by @gundermanc in
|
||||
[#17030](https://github.com/google-gemini/gemini-cli/pull/17030)
|
||||
- fix(cli): use OSC-52 clipboard copy in Windows Terminal by @Thomas-Shephard in
|
||||
[#16920](https://github.com/google-gemini/gemini-cli/pull/16920)
|
||||
- Fix: Process all parts in response chunks when thought is first by @pyrytakala
|
||||
in [#13539](https://github.com/google-gemini/gemini-cli/pull/13539)
|
||||
- fix(automation): fix jq quoting error in pr-triage.sh by @Kimsoo0119 in
|
||||
[#16958](https://github.com/google-gemini/gemini-cli/pull/16958)
|
||||
- refactor(core): decouple scheduler into orchestration, policy, and
|
||||
confirmation by @abhipatel12 in
|
||||
[#16895](https://github.com/google-gemini/gemini-cli/pull/16895)
|
||||
- feat: add /introspect slash command by @NTaylorMullen in
|
||||
[#17048](https://github.com/google-gemini/gemini-cli/pull/17048)
|
||||
- refactor(cli): centralize tool mapping and decouple legacy scheduler by
|
||||
@abhipatel12 in
|
||||
[#17044](https://github.com/google-gemini/gemini-cli/pull/17044)
|
||||
- fix(ui): ensure rationale renders before tool calls by @NTaylorMullen in
|
||||
[#17043](https://github.com/google-gemini/gemini-cli/pull/17043)
|
||||
- fix(workflows): use author_association for maintainer check by @bdmorgan in
|
||||
[#17060](https://github.com/google-gemini/gemini-cli/pull/17060)
|
||||
- fix return type of fireSessionStartEvent to defaultHookOutput by @ved015 in
|
||||
[#16833](https://github.com/google-gemini/gemini-cli/pull/16833)
|
||||
- feat(cli): add experiment gate for event-driven scheduler by @abhipatel12 in
|
||||
[#17055](https://github.com/google-gemini/gemini-cli/pull/17055)
|
||||
- feat(core): improve shell redirection transparency and security by
|
||||
@NTaylorMullen in
|
||||
[#16486](https://github.com/google-gemini/gemini-cli/pull/16486)
|
||||
- fix(core): deduplicate ModelInfo emission in GeminiClient by @NTaylorMullen in
|
||||
[#17075](https://github.com/google-gemini/gemini-cli/pull/17075)
|
||||
- docs(themes): remove unsupported DiffModified color key by @jw409 in
|
||||
[#17073](https://github.com/google-gemini/gemini-cli/pull/17073)
|
||||
- fix: update currentSequenceModel when modelChanged by @adamfweidman in
|
||||
[#17051](https://github.com/google-gemini/gemini-cli/pull/17051)
|
||||
- feat(core): enhanced anchored iterative context compression with
|
||||
self-verification by @rmedranollamas in
|
||||
[#15710](https://github.com/google-gemini/gemini-cli/pull/15710)
|
||||
- Fix mcp instructions by @chrstnb in
|
||||
[#16439](https://github.com/google-gemini/gemini-cli/pull/16439)
|
||||
- [A2A] Disable checkpointing if git is not installed by @cocosheng-g in
|
||||
[#16896](https://github.com/google-gemini/gemini-cli/pull/16896)
|
||||
- feat(admin): set admin.skills.enabled based on advancedFeaturesEnabled setting
|
||||
by @skeshive in
|
||||
[#17095](https://github.com/google-gemini/gemini-cli/pull/17095)
|
||||
- Test coverage for hook exit code cases by @gundermanc in
|
||||
[#17041](https://github.com/google-gemini/gemini-cli/pull/17041)
|
||||
- Revert "Revert "Update extension examples"" by @chrstnb in
|
||||
[#16445](https://github.com/google-gemini/gemini-cli/pull/16445)
|
||||
- fix(core): Provide compact, actionable errors for agent delegation failures by
|
||||
[#17227](https://github.com/google-gemini/gemini-cli/pull/17227)
|
||||
- feat(config): default enableEventDrivenScheduler to true by @abhipatel12 in
|
||||
[#17211](https://github.com/google-gemini/gemini-cli/pull/17211)
|
||||
- feat(hooks): enable hooks system by default by @abhipatel12 in
|
||||
[#17247](https://github.com/google-gemini/gemini-cli/pull/17247)
|
||||
- feat(core): Enable AgentRegistry to track all discovered subagents by
|
||||
@SandyTao520 in
|
||||
[#16493](https://github.com/google-gemini/gemini-cli/pull/16493)
|
||||
- fix: migrate BeforeModel and AfterModel hooks to HookSystem by @ved015 in
|
||||
[#16599](https://github.com/google-gemini/gemini-cli/pull/16599)
|
||||
- feat(admin): apply admin settings to gemini skills/mcp/extensions commands by
|
||||
@skeshive in [#17102](https://github.com/google-gemini/gemini-cli/pull/17102)
|
||||
- fix(core): update telemetry token count after session resume by @psinha40898
|
||||
in [#15491](https://github.com/google-gemini/gemini-cli/pull/15491)
|
||||
- Demote the subagent test to nightly by @gundermanc in
|
||||
[#17105](https://github.com/google-gemini/gemini-cli/pull/17105)
|
||||
- feat(plan): telemetry to track adoption and usage of plan mode by @Adib234 in
|
||||
[#16863](https://github.com/google-gemini/gemini-cli/pull/16863)
|
||||
- feat: Add flash lite utility fallback chain by @adamfweidman in
|
||||
[#17056](https://github.com/google-gemini/gemini-cli/pull/17056)
|
||||
- Fixes Windows crash: "Cannot resize a pty that has already exited" by @dzammit
|
||||
in [#15757](https://github.com/google-gemini/gemini-cli/pull/15757)
|
||||
- feat(core): Add initial eval for generalist agent. by @joshualitt in
|
||||
[#16856](https://github.com/google-gemini/gemini-cli/pull/16856)
|
||||
- feat(core): unify agent enabled and disabled flags by @SandyTao520 in
|
||||
[#17127](https://github.com/google-gemini/gemini-cli/pull/17127)
|
||||
- fix(core): resolve auto model in default strategy by @sehoon38 in
|
||||
[#17116](https://github.com/google-gemini/gemini-cli/pull/17116)
|
||||
- docs: update project context and pr-creator workflow by @NTaylorMullen in
|
||||
[#17119](https://github.com/google-gemini/gemini-cli/pull/17119)
|
||||
- fix(cli): send gemini-cli version as mcp client version by @dsp in
|
||||
[#13407](https://github.com/google-gemini/gemini-cli/pull/13407)
|
||||
- fix(cli): resolve Ctrl+Enter and Ctrl+J newline issues by @imadraude in
|
||||
[#17021](https://github.com/google-gemini/gemini-cli/pull/17021)
|
||||
- Remove missing sidebar item by @chrstnb in
|
||||
[#17145](https://github.com/google-gemini/gemini-cli/pull/17145)
|
||||
- feat(core): Ensure all properties in hooks object are event names. by
|
||||
@joshualitt in
|
||||
[#16870](https://github.com/google-gemini/gemini-cli/pull/16870)
|
||||
- fix(cli): fix newline support broken in previous PR by @scidomino in
|
||||
[#17159](https://github.com/google-gemini/gemini-cli/pull/17159)
|
||||
- Add interactive ValidationDialog for handling 403 VALIDATION_REQUIRED errors.
|
||||
by @gsquared94 in
|
||||
[#16231](https://github.com/google-gemini/gemini-cli/pull/16231)
|
||||
- Add Esc-Esc to clear prompt when it's not empty by @Adib234 in
|
||||
[#17131](https://github.com/google-gemini/gemini-cli/pull/17131)
|
||||
- Avoid spurious warnings about unexpected renders triggered by appEvents and
|
||||
coreEvents. by @jacob314 in
|
||||
[#17160](https://github.com/google-gemini/gemini-cli/pull/17160)
|
||||
- fix(cli): resolve home/end keybinding conflict by @scidomino in
|
||||
[#17124](https://github.com/google-gemini/gemini-cli/pull/17124)
|
||||
- fix(cli): display 'http' type on mcp list by @pamanta in
|
||||
[#16915](https://github.com/google-gemini/gemini-cli/pull/16915)
|
||||
- fix bad fallback logic external editor logic by @scidomino in
|
||||
[#17166](https://github.com/google-gemini/gemini-cli/pull/17166)
|
||||
- Fix bug where System scopes weren't migrated. by @jacob314 in
|
||||
[#17174](https://github.com/google-gemini/gemini-cli/pull/17174)
|
||||
- Fix mcp tool lookup in tool registry by @werdnum in
|
||||
[#17054](https://github.com/google-gemini/gemini-cli/pull/17054)
|
||||
[#17253](https://github.com/google-gemini/gemini-cli/pull/17253)
|
||||
- feat(core): Have subagents use a JSON schema type for input. by @joshualitt in
|
||||
[#17152](https://github.com/google-gemini/gemini-cli/pull/17152)
|
||||
- feat: replace large text pastes with [Pasted Text: X lines] placeholder by
|
||||
@jackwotherspoon in
|
||||
[#16422](https://github.com/google-gemini/gemini-cli/pull/16422)
|
||||
- security(hooks): Wrap hook-injected context in distinct XML tags by @yunaseoul
|
||||
in [#17237](https://github.com/google-gemini/gemini-cli/pull/17237)
|
||||
- Enable the ability to queue specific nightly eval tests by @gundermanc in
|
||||
[#17262](https://github.com/google-gemini/gemini-cli/pull/17262)
|
||||
- docs(hooks): comprehensive update of hook documentation and specs by
|
||||
@abhipatel12 in
|
||||
[#16816](https://github.com/google-gemini/gemini-cli/pull/16816)
|
||||
- refactor: improve large text paste placeholder by @jacob314 in
|
||||
[#17269](https://github.com/google-gemini/gemini-cli/pull/17269)
|
||||
- feat: implement /rewind command by @Adib234 in
|
||||
[#15720](https://github.com/google-gemini/gemini-cli/pull/15720)
|
||||
- Feature/jetbrains ide detection by @SoLoHiC in
|
||||
[#16243](https://github.com/google-gemini/gemini-cli/pull/16243)
|
||||
- docs: update typo in mcp-server.md file by @schifferl in
|
||||
[#17099](https://github.com/google-gemini/gemini-cli/pull/17099)
|
||||
- Sanitize command names and descriptions by @ehedlund in
|
||||
[#17228](https://github.com/google-gemini/gemini-cli/pull/17228)
|
||||
- fix(auth): don't crash when initial auth fails by @skeshive in
|
||||
[#17308](https://github.com/google-gemini/gemini-cli/pull/17308)
|
||||
- Added image pasting capabilities for Wayland and X11 on Linux by @devr0306 in
|
||||
[#17144](https://github.com/google-gemini/gemini-cli/pull/17144)
|
||||
- feat: add AskUser tool schema by @jackwotherspoon in
|
||||
[#16988](https://github.com/google-gemini/gemini-cli/pull/16988)
|
||||
- fix cli settings: resolve layout jitter in settings bar by @Mag1ck in
|
||||
[#16256](https://github.com/google-gemini/gemini-cli/pull/16256)
|
||||
- fix: show whitespace changes in edit tool diffs by @Ujjiyara in
|
||||
[#17213](https://github.com/google-gemini/gemini-cli/pull/17213)
|
||||
- Remove redundant calls setting linuxClipboardTool. getUserLinuxClipboardTool()
|
||||
now handles the caching internally by @jacob314 in
|
||||
[#17320](https://github.com/google-gemini/gemini-cli/pull/17320)
|
||||
- ci: allow failure in evals-nightly run step by @gundermanc in
|
||||
[#17319](https://github.com/google-gemini/gemini-cli/pull/17319)
|
||||
- feat(cli): Add state management and plumbing for agent configuration dialog by
|
||||
@SandyTao520 in
|
||||
[#17259](https://github.com/google-gemini/gemini-cli/pull/17259)
|
||||
- bug: fix ide-client connection to ide-companion when inside docker via
|
||||
ssh/devcontainer by @kapsner in
|
||||
[#15049](https://github.com/google-gemini/gemini-cli/pull/15049)
|
||||
- Emit correct newline type return by @scidomino in
|
||||
[#17331](https://github.com/google-gemini/gemini-cli/pull/17331)
|
||||
- New skill: docs-writer by @g-samroberts in
|
||||
[#17268](https://github.com/google-gemini/gemini-cli/pull/17268)
|
||||
- fix(core): Resolve AbortSignal MaxListenersExceededWarning (#5950) by
|
||||
@spencer426 in
|
||||
[#16735](https://github.com/google-gemini/gemini-cli/pull/16735)
|
||||
- Disable tips after 10 runs by @Adib234 in
|
||||
[#17101](https://github.com/google-gemini/gemini-cli/pull/17101)
|
||||
- Fix so rewind starts at the bottom and loadHistory refreshes static content.
|
||||
by @jacob314 in
|
||||
[#17335](https://github.com/google-gemini/gemini-cli/pull/17335)
|
||||
- feat(core): Remove legacy settings. by @joshualitt in
|
||||
[#17244](https://github.com/google-gemini/gemini-cli/pull/17244)
|
||||
- feat(plan): add 'communicate' tool kind by @jerop in
|
||||
[#17341](https://github.com/google-gemini/gemini-cli/pull/17341)
|
||||
- feat(routing): A/B Test Numerical Complexity Scoring for Gemini 3 by
|
||||
@mattKorwel in
|
||||
[#16041](https://github.com/google-gemini/gemini-cli/pull/16041)
|
||||
- feat(plan): update UI Theme for Plan Mode by @Adib234 in
|
||||
[#17243](https://github.com/google-gemini/gemini-cli/pull/17243)
|
||||
- fix(ui): stabilize rendering during terminal resize in alternate buffer by
|
||||
@lkk214 in [#15783](https://github.com/google-gemini/gemini-cli/pull/15783)
|
||||
- feat(cli): add /agents config command and improve agent discovery by
|
||||
@SandyTao520 in
|
||||
[#17342](https://github.com/google-gemini/gemini-cli/pull/17342)
|
||||
- feat(mcp): add enable/disable commands for MCP servers (#11057) by @jasmeetsb
|
||||
in [#16299](https://github.com/google-gemini/gemini-cli/pull/16299)
|
||||
- fix(cli)!: Default to interactive mode for positional arguments by
|
||||
@ishaanxgupta in
|
||||
[#16329](https://github.com/google-gemini/gemini-cli/pull/16329)
|
||||
- Fix issue #17080 by @jacob314 in
|
||||
[#17100](https://github.com/google-gemini/gemini-cli/pull/17100)
|
||||
- feat(core): Refresh agents after loading an extension. by @joshualitt in
|
||||
[#17355](https://github.com/google-gemini/gemini-cli/pull/17355)
|
||||
- fix(cli): include source in policy rule display by @allenhutchison in
|
||||
[#17358](https://github.com/google-gemini/gemini-cli/pull/17358)
|
||||
- fix: remove obsolete CloudCode PerDay quota and 120s terminal threshold by
|
||||
@gsquared94 in
|
||||
[#17236](https://github.com/google-gemini/gemini-cli/pull/17236)
|
||||
- Refactor subagent delegation to be one tool per agent by @gundermanc in
|
||||
[#17346](https://github.com/google-gemini/gemini-cli/pull/17346)
|
||||
- fix(core): Include MCP server name in OAuth message by @jerop in
|
||||
[#17351](https://github.com/google-gemini/gemini-cli/pull/17351)
|
||||
- Fix pr-triage.sh script to update pull requests with tags "help wanted" and
|
||||
"maintainer only" by @jacob314 in
|
||||
[#17324](https://github.com/google-gemini/gemini-cli/pull/17324)
|
||||
- feat(plan): implement simple workflow for planning in main agent by @jerop in
|
||||
[#17326](https://github.com/google-gemini/gemini-cli/pull/17326)
|
||||
- fix: exit with non-zero code when esbuild is missing by @yuvrajangadsingh in
|
||||
[#16967](https://github.com/google-gemini/gemini-cli/pull/16967)
|
||||
- fix: ensure @docs/cli/custom-commands.md UI message ordering and test by
|
||||
@medic-code in
|
||||
[#12038](https://github.com/google-gemini/gemini-cli/pull/12038)
|
||||
- fix(core): add alternative command names for Antigravity editor detec… by
|
||||
@BaeSeokJae in
|
||||
[#16829](https://github.com/google-gemini/gemini-cli/pull/16829)
|
||||
- Refactor: Migrate CLI appEvents to Core coreEvents by @Adib234 in
|
||||
[#15737](https://github.com/google-gemini/gemini-cli/pull/15737)
|
||||
- fix(core): await MCP initialization in non-interactive mode by @Ratish1 in
|
||||
[#17390](https://github.com/google-gemini/gemini-cli/pull/17390)
|
||||
- Fix modifyOtherKeys enablement on unsupported terminals by @seekskyworld in
|
||||
[#16714](https://github.com/google-gemini/gemini-cli/pull/16714)
|
||||
- fix(core): gracefully handle disk full errors in chat recording by
|
||||
@godwiniheuwa in
|
||||
[#17305](https://github.com/google-gemini/gemini-cli/pull/17305)
|
||||
- fix(oauth): update oauth to use 127.0.0.1 instead of localhost by @skeshive in
|
||||
[#17388](https://github.com/google-gemini/gemini-cli/pull/17388)
|
||||
- fix(core): use RFC 9728 compliant path-based OAuth protected resource
|
||||
discovery by @vrv in
|
||||
[#15756](https://github.com/google-gemini/gemini-cli/pull/15756)
|
||||
- Update Code Wiki README badge by @PatoBeltran in
|
||||
[#15229](https://github.com/google-gemini/gemini-cli/pull/15229)
|
||||
- Add conda installation instructions for Gemini CLI by @ishaanxgupta in
|
||||
[#16921](https://github.com/google-gemini/gemini-cli/pull/16921)
|
||||
- chore(refactor): extract BaseSettingsDialog component by @SandyTao520 in
|
||||
[#17369](https://github.com/google-gemini/gemini-cli/pull/17369)
|
||||
- fix(cli): preserve input text when declining tool approval (#15624) by
|
||||
@ManojINaik in
|
||||
[#15659](https://github.com/google-gemini/gemini-cli/pull/15659)
|
||||
- chore: upgrade dep: diff 7.0.0-> 8.0.3 by @scidomino in
|
||||
[#17403](https://github.com/google-gemini/gemini-cli/pull/17403)
|
||||
- feat: add AskUserDialog for UI component of AskUser tool by @jackwotherspoon
|
||||
in [#17344](https://github.com/google-gemini/gemini-cli/pull/17344)
|
||||
- feat(ui): display user tier in about command by @sehoon38 in
|
||||
[#17400](https://github.com/google-gemini/gemini-cli/pull/17400)
|
||||
- feat: add clearContext to AfterAgent hooks by @jackwotherspoon in
|
||||
[#16574](https://github.com/google-gemini/gemini-cli/pull/16574)
|
||||
- fix(cli): change image paste location to global temp directory (#17396) by
|
||||
@devr0306 in [#17396](https://github.com/google-gemini/gemini-cli/pull/17396)
|
||||
- Fix line endings issue with Notice file by @scidomino in
|
||||
[#17417](https://github.com/google-gemini/gemini-cli/pull/17417)
|
||||
- feat(plan): implement persistent approvalMode setting by @Adib234 in
|
||||
[#17350](https://github.com/google-gemini/gemini-cli/pull/17350)
|
||||
- feat(ui): Move keyboard handling into BaseSettingsDialog by @SandyTao520 in
|
||||
[#17404](https://github.com/google-gemini/gemini-cli/pull/17404)
|
||||
- Allow prompt queueing during MCP initialization by @Adib234 in
|
||||
[#17395](https://github.com/google-gemini/gemini-cli/pull/17395)
|
||||
- feat: implement AgentConfigDialog for /agents config command by @SandyTao520
|
||||
in [#17370](https://github.com/google-gemini/gemini-cli/pull/17370)
|
||||
- fix(agents): default to all tools when tool list is omitted in subagents by
|
||||
@gundermanc in
|
||||
[#17422](https://github.com/google-gemini/gemini-cli/pull/17422)
|
||||
- feat(cli): Moves tool confirmations to a queue UX by @abhipatel12 in
|
||||
[#17276](https://github.com/google-gemini/gemini-cli/pull/17276)
|
||||
- fix(core): hide user tier name by @sehoon38 in
|
||||
[#17418](https://github.com/google-gemini/gemini-cli/pull/17418)
|
||||
- feat: Enforce unified folder trust for /directory add by @galz10 in
|
||||
[#17359](https://github.com/google-gemini/gemini-cli/pull/17359)
|
||||
- migrate fireToolNotificationHook to hookSystem by @ved015 in
|
||||
[#17398](https://github.com/google-gemini/gemini-cli/pull/17398)
|
||||
- Clean up dead code by @scidomino in
|
||||
[#17443](https://github.com/google-gemini/gemini-cli/pull/17443)
|
||||
- feat(workflow): add stale pull request closer with linked-issue enforcement by
|
||||
@bdmorgan in [#17449](https://github.com/google-gemini/gemini-cli/pull/17449)
|
||||
- feat(workflow): expand stale-exempt labels to include help wanted and Public
|
||||
Roadmap by @bdmorgan in
|
||||
[#17459](https://github.com/google-gemini/gemini-cli/pull/17459)
|
||||
- chore(workflow): remove redundant label-enforcer workflow by @bdmorgan in
|
||||
[#17460](https://github.com/google-gemini/gemini-cli/pull/17460)
|
||||
- Resolves the confusing error message `ripgrep exited with code null that
|
||||
occurs when a search operation is cancelled or aborted by @maximmasiutin in
|
||||
[#14267](https://github.com/google-gemini/gemini-cli/pull/14267)
|
||||
- fix: detect pnpm/pnpx in ~/.local by @rwakulszowa in
|
||||
[#15254](https://github.com/google-gemini/gemini-cli/pull/15254)
|
||||
- docs: Add instructions for MacPorts and uninstall instructions for Homebrew by
|
||||
@breun in [#17412](https://github.com/google-gemini/gemini-cli/pull/17412)
|
||||
- docs(hooks): clarify mandatory 'type' field and update hook schema
|
||||
documentation by @abhipatel12 in
|
||||
[#17499](https://github.com/google-gemini/gemini-cli/pull/17499)
|
||||
- Improve error messages on failed onboarding by @gsquared94 in
|
||||
[#17357](https://github.com/google-gemini/gemini-cli/pull/17357)
|
||||
- Follow up to "enableInteractiveShell for external tooling relying on a2a
|
||||
server" by @DavidAPierce in
|
||||
[#17130](https://github.com/google-gemini/gemini-cli/pull/17130)
|
||||
- Fix/issue 17070 by @alih552 in
|
||||
[#17242](https://github.com/google-gemini/gemini-cli/pull/17242)
|
||||
- fix(core): handle URI-encoded workspace paths in IdeClient by @dong-jun-shin
|
||||
in [#17476](https://github.com/google-gemini/gemini-cli/pull/17476)
|
||||
- feat(cli): add quick clear input shortcuts in vim mode by @harshanadim in
|
||||
[#17470](https://github.com/google-gemini/gemini-cli/pull/17470)
|
||||
- feat(core): optimize shell tool llmContent output format by @SandyTao520 in
|
||||
[#17538](https://github.com/google-gemini/gemini-cli/pull/17538)
|
||||
- Fix bug in detecting already added paths. by @jacob314 in
|
||||
[#17430](https://github.com/google-gemini/gemini-cli/pull/17430)
|
||||
- feat(scheduler): support multi-scheduler tool aggregation and nested call IDs
|
||||
by @abhipatel12 in
|
||||
[#17429](https://github.com/google-gemini/gemini-cli/pull/17429)
|
||||
- feat(agents): implement first-run experience for project-level sub-agents by
|
||||
@gundermanc in
|
||||
[#17266](https://github.com/google-gemini/gemini-cli/pull/17266)
|
||||
- Update extensions docs by @chrstnb in
|
||||
[#16093](https://github.com/google-gemini/gemini-cli/pull/16093)
|
||||
- Docs: Refactor left nav on the website by @jkcinouye in
|
||||
[#17558](https://github.com/google-gemini/gemini-cli/pull/17558)
|
||||
- fix(core): stream grep/ripgrep output to prevent OOM by @adamfweidman in
|
||||
[#17146](https://github.com/google-gemini/gemini-cli/pull/17146)
|
||||
- feat(plan): add persistent plan file storage by @jerop in
|
||||
[#17563](https://github.com/google-gemini/gemini-cli/pull/17563)
|
||||
- feat(agents): migrate subagents to event-driven scheduler by @abhipatel12 in
|
||||
[#17567](https://github.com/google-gemini/gemini-cli/pull/17567)
|
||||
- Fix extensions config error by @chrstnb in
|
||||
[#17580](https://github.com/google-gemini/gemini-cli/pull/17580)
|
||||
- fix(plan): remove subagent invocation from plan mode by @jerop in
|
||||
[#17593](https://github.com/google-gemini/gemini-cli/pull/17593)
|
||||
- feat(ui): add solid background color option for input prompt by @jacob314 in
|
||||
[#16563](https://github.com/google-gemini/gemini-cli/pull/16563)
|
||||
- feat(plan): refresh system prompt when approval mode changes (Shift+Tab) by
|
||||
@jerop in [#17585](https://github.com/google-gemini/gemini-cli/pull/17585)
|
||||
- feat(cli): add global setting to disable UI spinners by @galz10 in
|
||||
[#17234](https://github.com/google-gemini/gemini-cli/pull/17234)
|
||||
- fix(security): enforce strict policy directory permissions by @yunaseoul in
|
||||
[#17353](https://github.com/google-gemini/gemini-cli/pull/17353)
|
||||
- test(core): fix tests in windows by @scidomino in
|
||||
[#17592](https://github.com/google-gemini/gemini-cli/pull/17592)
|
||||
- feat(mcp/extensions): Allow users to selectively enable/disable MCP servers
|
||||
included in an extension( Issue #11057 & #17402) by @jasmeetsb in
|
||||
[#17434](https://github.com/google-gemini/gemini-cli/pull/17434)
|
||||
- Always map mac keys, even on other platforms by @scidomino in
|
||||
[#17618](https://github.com/google-gemini/gemini-cli/pull/17618)
|
||||
- Ctrl-O by @jacob314 in
|
||||
[#17617](https://github.com/google-gemini/gemini-cli/pull/17617)
|
||||
- feat(plan): update cycling order of approval modes by @Adib234 in
|
||||
[#17622](https://github.com/google-gemini/gemini-cli/pull/17622)
|
||||
- fix(cli): restore 'Modify with editor' option in external terminals by
|
||||
@abhipatel12 in
|
||||
[#17621](https://github.com/google-gemini/gemini-cli/pull/17621)
|
||||
- Slash command for helping in debugging by @gundermanc in
|
||||
[#17609](https://github.com/google-gemini/gemini-cli/pull/17609)
|
||||
- feat: add double-click to expand/collapse large paste placeholders by
|
||||
@jackwotherspoon in
|
||||
[#17471](https://github.com/google-gemini/gemini-cli/pull/17471)
|
||||
- refactor(cli): migrate non-interactive flow to event-driven scheduler by
|
||||
@abhipatel12 in
|
||||
[#17572](https://github.com/google-gemini/gemini-cli/pull/17572)
|
||||
- fix: loadcodeassist eligible tiers getting ignored for unlicensed users
|
||||
(regression) by @gsquared94 in
|
||||
[#17581](https://github.com/google-gemini/gemini-cli/pull/17581)
|
||||
- chore(core): delete legacy nonInteractiveToolExecutor by @abhipatel12 in
|
||||
[#17573](https://github.com/google-gemini/gemini-cli/pull/17573)
|
||||
- feat(core): enforce server prefixes for MCP tools in agent definitions by
|
||||
@abhipatel12 in
|
||||
[#17574](https://github.com/google-gemini/gemini-cli/pull/17574)
|
||||
- feat (mcp): Refresh MCP prompts on list changed notification by @MrLesk in
|
||||
[#14863](https://github.com/google-gemini/gemini-cli/pull/14863)
|
||||
- feat(ui): pretty JSON rendering tool outputs by @medic-code in
|
||||
[#9767](https://github.com/google-gemini/gemini-cli/pull/9767)
|
||||
- Fix iterm alternate buffer mode issue rendering backgrounds by @jacob314 in
|
||||
[#17634](https://github.com/google-gemini/gemini-cli/pull/17634)
|
||||
- feat(cli): add gemini extensions list --output-format=json by @AkihiroSuda in
|
||||
[#14479](https://github.com/google-gemini/gemini-cli/pull/14479)
|
||||
- fix(extensions): add .gitignore to extension templates by @godwiniheuwa in
|
||||
[#17293](https://github.com/google-gemini/gemini-cli/pull/17293)
|
||||
- paste transform followup by @jacob314 in
|
||||
[#17624](https://github.com/google-gemini/gemini-cli/pull/17624)
|
||||
- refactor: rename formatMemoryUsage to formatBytes by @Nubebuster in
|
||||
[#14997](https://github.com/google-gemini/gemini-cli/pull/14997)
|
||||
- chore: remove extra top margin from /hooks and /extensions by @jackwotherspoon
|
||||
in [#17663](https://github.com/google-gemini/gemini-cli/pull/17663)
|
||||
- feat(cli): add oncall command for issue triage by @sehoon38 in
|
||||
[#17661](https://github.com/google-gemini/gemini-cli/pull/17661)
|
||||
- Fix sidebar issue for extensions link by @chrstnb in
|
||||
[#17668](https://github.com/google-gemini/gemini-cli/pull/17668)
|
||||
- Change formatting to prevent UI redressing attacks by @scidomino in
|
||||
[#17611](https://github.com/google-gemini/gemini-cli/pull/17611)
|
||||
- Fix cluster of bugs in the settings dialog. by @jacob314 in
|
||||
[#17628](https://github.com/google-gemini/gemini-cli/pull/17628)
|
||||
- Update sidebar to resolve site build issues by @chrstnb in
|
||||
[#17674](https://github.com/google-gemini/gemini-cli/pull/17674)
|
||||
- fix(admin): fix a few bugs related to admin controls by @skeshive in
|
||||
[#17590](https://github.com/google-gemini/gemini-cli/pull/17590)
|
||||
- revert bad changes to tests by @scidomino in
|
||||
[#17673](https://github.com/google-gemini/gemini-cli/pull/17673)
|
||||
- feat(cli): show candidate issue state reason and duplicate status in triage by
|
||||
@sehoon38 in [#17676](https://github.com/google-gemini/gemini-cli/pull/17676)
|
||||
- Fix missing slash commands when Gemini CLI is in a project with a package.json
|
||||
that doesn't follow semantic versioning by @Adib234 in
|
||||
[#17561](https://github.com/google-gemini/gemini-cli/pull/17561)
|
||||
- feat(core): Model family-specific system prompts by @joshualitt in
|
||||
[#17614](https://github.com/google-gemini/gemini-cli/pull/17614)
|
||||
- Sub-agents documentation. by @gundermanc in
|
||||
[#16639](https://github.com/google-gemini/gemini-cli/pull/16639)
|
||||
- feat: wire up AskUserTool with dialog by @jackwotherspoon in
|
||||
[#17411](https://github.com/google-gemini/gemini-cli/pull/17411)
|
||||
- Load extension settings for hooks, agents, skills by @chrstnb in
|
||||
[#17245](https://github.com/google-gemini/gemini-cli/pull/17245)
|
||||
- Fix issue where Gemini CLI can make changes when simply asked a question by
|
||||
@gundermanc in
|
||||
[#17608](https://github.com/google-gemini/gemini-cli/pull/17608)
|
||||
- Update docs-writer skill for editing and add style guide for reference. by
|
||||
@g-samroberts in
|
||||
[#17669](https://github.com/google-gemini/gemini-cli/pull/17669)
|
||||
- fix(ux): have user message display a short path for pasted images by @devr0306
|
||||
in [#17613](https://github.com/google-gemini/gemini-cli/pull/17613)
|
||||
- feat(plan): enable AskUser tool in Plan mode for clarifying questions by
|
||||
@jerop in [#17694](https://github.com/google-gemini/gemini-cli/pull/17694)
|
||||
- GEMINI.md polish by @jacob314 in
|
||||
[#17680](https://github.com/google-gemini/gemini-cli/pull/17680)
|
||||
- refactor(core): centralize path validation and allow temp dir access for tools
|
||||
by @NTaylorMullen in
|
||||
[#17185](https://github.com/google-gemini/gemini-cli/pull/17185)
|
||||
- feat(skills): promote Agent Skills to stable by @abhipatel12 in
|
||||
[#17693](https://github.com/google-gemini/gemini-cli/pull/17693)
|
||||
- refactor(cli): keyboard handling and AskUserDialog by @jacob314 in
|
||||
[#17414](https://github.com/google-gemini/gemini-cli/pull/17414)
|
||||
- docs: Add Experimental Remote Agent Docs by @adamfweidman in
|
||||
[#17697](https://github.com/google-gemini/gemini-cli/pull/17697)
|
||||
- revert: promote Agent Skills to stable (#17693) by @abhipatel12 in
|
||||
[#17712](https://github.com/google-gemini/gemini-cli/pull/17712)
|
||||
- feat(ux) Expandable (ctrl-O) and scrollable approvals in alternate buffer
|
||||
mode. by @jacob314 in
|
||||
[#17640](https://github.com/google-gemini/gemini-cli/pull/17640)
|
||||
- feat(skills): promote skills settings to stable by @abhipatel12 in
|
||||
[#17713](https://github.com/google-gemini/gemini-cli/pull/17713)
|
||||
- fix(cli): Preserve settings dialog focus when searching by @SandyTao520 in
|
||||
[#17701](https://github.com/google-gemini/gemini-cli/pull/17701)
|
||||
- feat(ui): add terminal cursor support by @jacob314 in
|
||||
[#17711](https://github.com/google-gemini/gemini-cli/pull/17711)
|
||||
- docs(skills): remove experimental labels and update tutorials by @abhipatel12
|
||||
in [#17714](https://github.com/google-gemini/gemini-cli/pull/17714)
|
||||
- docs: remove 'experimental' syntax for hooks in docs by @abhipatel12 in
|
||||
[#17660](https://github.com/google-gemini/gemini-cli/pull/17660)
|
||||
- Add support for an additional exclusion file besides .gitignore and
|
||||
.geminiignore by @alisa-alisa in
|
||||
[#16487](https://github.com/google-gemini/gemini-cli/pull/16487)
|
||||
- feat: add review-frontend-and-fix command by @galz10 in
|
||||
[#17707](https://github.com/google-gemini/gemini-cli/pull/17707)
|
||||
|
||||
**Full changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.25.0-preview.4...v0.26.0-preview.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.26.0-preview.5...v0.27.0-preview.0
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# CLI cheatsheet
|
||||
|
||||
This page provides a reference for commonly used Gemini CLI commands, options,
|
||||
and parameters.
|
||||
|
||||
## CLI commands
|
||||
|
||||
| Command | Description | Example |
|
||||
| ---------------------------------- | ---------------------------------- | --------------------------------------------------- |
|
||||
| `gemini` | Start interactive REPL | `gemini` |
|
||||
| `gemini "query"` | Query non-interactively, then exit | `gemini "explain this project"` |
|
||||
| `gemini -p "query"` | Query via SDK, then exit | `gemini -p "explain this function"` |
|
||||
| `cat file \| gemini -p "query"` | Process piped content | `cat logs.txt \| gemini -p "explain"` |
|
||||
| `gemini -i "query"` | Execute and continue interactively | `gemini -i "What is the purpose of this project?"` |
|
||||
| `gemini -r "latest"` | Continue most recent session | `gemini -r "latest"` |
|
||||
| `gemini -r "latest" "query"` | Continue session with a new prompt | `gemini -r "latest" "Check for type errors"` |
|
||||
| `gemini -r "<session-id>" "query"` | Resume session by ID | `gemini -r "abc123" "Finish this PR"` |
|
||||
| `gemini update` | Update to latest version | `gemini update` |
|
||||
| `gemini extensions` | Manage extensions | See [Extensions Management](#extensions-management) |
|
||||
| `gemini mcp` | Configure MCP servers | See [MCP Server Management](#mcp-server-management) |
|
||||
|
||||
### Positional arguments
|
||||
|
||||
| Argument | Type | Description |
|
||||
| -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `query` | string (variadic) | Positional prompt. Defaults to one-shot mode. Use `-i/--prompt-interactive` to execute and continue interactively. |
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Option | Alias | Type | Default | Description |
|
||||
| -------------------------------- | ----- | ------- | --------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
|
||||
| `--version` | `-v` | - | - | Show CLI version number and exit |
|
||||
| `--help` | `-h` | - | - | Show help information |
|
||||
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
|
||||
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
|
||||
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
|
||||
| `--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` |
|
||||
| `--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.** |
|
||||
| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) |
|
||||
| `--allowed-tools` | - | array | - | Tools that are allowed to run without confirmation (comma-separated or multiple flags) |
|
||||
| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) |
|
||||
| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit |
|
||||
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) |
|
||||
| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit |
|
||||
| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) |
|
||||
| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
|
||||
| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility |
|
||||
| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` |
|
||||
|
||||
## Model selection
|
||||
|
||||
The `--model` (or `-m`) flag allows you to specify which Gemini model to use.
|
||||
You can use either model aliases (user-friendly names) or concrete model names.
|
||||
|
||||
### Model aliases
|
||||
|
||||
These are convenient shortcuts that map to specific models:
|
||||
|
||||
| Alias | Resolves To | Description |
|
||||
| ------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `auto` | `gemini-2.5-pro` or `gemini-3-pro-preview` | **Default.** Resolves to the preview model if preview features are enabled, otherwise resolves to the standard pro model. |
|
||||
| `pro` | `gemini-2.5-pro` or `gemini-3-pro-preview` | For complex reasoning tasks. Uses preview model if enabled. |
|
||||
| `flash` | `gemini-2.5-flash` | Fast, balanced model for most tasks. |
|
||||
| `flash-lite` | `gemini-2.5-flash-lite` | Fastest model for simple tasks. |
|
||||
|
||||
## Extensions management
|
||||
|
||||
| Command | Description | Example |
|
||||
| -------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| `gemini extensions install <source>` | Install extension from Git URL or local path | `gemini extensions install https://github.com/user/my-extension` |
|
||||
| `gemini extensions install <source> --ref <ref>` | Install from specific branch/tag/commit | `gemini extensions install https://github.com/user/my-extension --ref develop` |
|
||||
| `gemini extensions install <source> --auto-update` | Install with auto-update enabled | `gemini extensions install https://github.com/user/my-extension --auto-update` |
|
||||
| `gemini extensions uninstall <name>` | Uninstall one or more extensions | `gemini extensions uninstall my-extension` |
|
||||
| `gemini extensions list` | List all installed extensions | `gemini extensions list` |
|
||||
| `gemini extensions update <name>` | Update a specific extension | `gemini extensions update my-extension` |
|
||||
| `gemini extensions update --all` | Update all extensions | `gemini extensions update --all` |
|
||||
| `gemini extensions enable <name>` | Enable an extension | `gemini extensions enable my-extension` |
|
||||
| `gemini extensions disable <name>` | Disable an extension | `gemini extensions disable my-extension` |
|
||||
| `gemini extensions link <path>` | Link local extension for development | `gemini extensions link /path/to/extension` |
|
||||
| `gemini extensions new <path>` | Create new extension from template | `gemini extensions new ./my-extension` |
|
||||
| `gemini extensions validate <path>` | Validate extension structure | `gemini extensions validate ./my-extension` |
|
||||
|
||||
See [Extensions Documentation](../extensions/index.md) for more details.
|
||||
|
||||
## MCP server management
|
||||
|
||||
| Command | Description | Example |
|
||||
| ------------------------------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `gemini mcp add <name> <command>` | Add stdio-based MCP server | `gemini mcp add github npx -y @modelcontextprotocol/server-github` |
|
||||
| `gemini mcp add <name> <url> --transport http` | Add HTTP-based MCP server | `gemini mcp add api-server http://localhost:3000 --transport http` |
|
||||
| `gemini mcp add <name> <command> --env KEY=value` | Add with environment variables | `gemini mcp add slack node server.js --env SLACK_TOKEN=xoxb-xxx` |
|
||||
| `gemini mcp add <name> <command> --scope user` | Add with user scope | `gemini mcp add db node db-server.js --scope user` |
|
||||
| `gemini mcp add <name> <command> --include-tools tool1,tool2` | Add with specific tools | `gemini mcp add github npx -y @modelcontextprotocol/server-github --include-tools list_repos,get_pr` |
|
||||
| `gemini mcp remove <name>` | Remove an MCP server | `gemini mcp remove github` |
|
||||
| `gemini mcp list` | List all configured MCP servers | `gemini mcp list` |
|
||||
|
||||
See [MCP Server Integration](../tools/mcp-server.md) for more details.
|
||||
+139
-84
@@ -10,6 +10,14 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
### Built-in Commands
|
||||
|
||||
- **`/about`**
|
||||
- **Description:** Show version info. Please share this information when
|
||||
filing issues.
|
||||
|
||||
- **`/auth`**
|
||||
- **Description:** Open a dialog that lets you change the authentication
|
||||
method.
|
||||
|
||||
- **`/bug`**
|
||||
- **Description:** File an issue about Gemini CLI. By default, the issue is
|
||||
filed within the GitHub repository for Gemini CLI. The string you enter
|
||||
@@ -22,10 +30,21 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
conversation state interactively, or resuming a previous state from a later
|
||||
session.
|
||||
- **Sub-commands:**
|
||||
- **`save`**
|
||||
- **`delete <tag>`**
|
||||
- **Description:** Deletes a saved conversation checkpoint.
|
||||
- **`list`**
|
||||
- **Description:** Lists available tags for chat state resumption.
|
||||
- **Note:** This command only lists chats saved within the current
|
||||
project. Because chat history is project-scoped, chats saved in other
|
||||
project directories will not be displayed.
|
||||
- **`resume <tag>`**
|
||||
- **Description:** Resumes a conversation from a previous save.
|
||||
- **Note:** You can only resume chats that were saved within the current
|
||||
project. To resume a chat from a different project, you must run the
|
||||
Gemini CLI from that project's directory.
|
||||
- **`save <tag>`**
|
||||
- **Description:** Saves the current conversation history. You must add a
|
||||
`<tag>` for identifying the conversation state.
|
||||
- **Usage:** `/chat save <tag>`
|
||||
- **Details on checkpoint location:** The default locations for saved chat
|
||||
checkpoints are:
|
||||
- Linux/macOS: `~/.gemini/tmp/<project_hash>/`
|
||||
@@ -37,25 +56,11 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
conversation states. For automatic checkpoints created before file
|
||||
modifications, see the
|
||||
[Checkpointing documentation](../cli/checkpointing.md).
|
||||
- **`resume`**
|
||||
- **Description:** Resumes a conversation from a previous save.
|
||||
- **Usage:** `/chat resume <tag>`
|
||||
- **Note:** You can only resume chats that were saved within the current
|
||||
project. To resume a chat from a different project, you must run the
|
||||
Gemini CLI from that project's directory.
|
||||
- **`list`**
|
||||
- **Description:** Lists available tags for chat state resumption.
|
||||
- **Note:** This command only lists chats saved within the current
|
||||
project. Because chat history is project-scoped, chats saved in other
|
||||
project directories will not be displayed.
|
||||
- **`delete`**
|
||||
- **Description:** Deletes a saved conversation checkpoint.
|
||||
- **Usage:** `/chat delete <tag>`
|
||||
- **`share`**
|
||||
- **`share [filename]`**
|
||||
- **Description** Writes the current conversation to a provided Markdown
|
||||
or JSON file.
|
||||
- **Usage** `/chat share file.md` or `/chat share file.json`. If no
|
||||
filename is provided, then the CLI will generate one.
|
||||
or JSON file. If no filename is provided, then the CLI will generate
|
||||
one.
|
||||
- **Usage** `/chat share file.md` or `/chat share file.json`.
|
||||
|
||||
- **`/clear`**
|
||||
- **Description:** Clear the terminal screen, including the visible session
|
||||
@@ -98,6 +103,9 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
`--include-directories`.
|
||||
- **Usage:** `/directory show`
|
||||
|
||||
- **`/docs`**
|
||||
- **Description:** Open the Gemini CLI documentation in your browser.
|
||||
|
||||
- **`/editor`**
|
||||
- **Description:** Open a dialog for selecting supported editors.
|
||||
|
||||
@@ -109,30 +117,65 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **Description:** Display help information about Gemini CLI, including
|
||||
available commands and their usage.
|
||||
|
||||
- **`/hooks`**
|
||||
- **Description:** Manage hooks, which allow you to intercept and customize
|
||||
Gemini CLI behavior at specific lifecycle events.
|
||||
- **Sub-commands:**
|
||||
- **`disable-all`**:
|
||||
- **Description:** Disable all enabled hooks.
|
||||
- **`disable <hook-name>`**:
|
||||
- **Description:** Disable a hook by name.
|
||||
- **`enable-all`**:
|
||||
- **Description:** Enable all disabled hooks.
|
||||
- **`enable <hook-name>`**:
|
||||
- **Description:** Enable a hook by name.
|
||||
- **`list`** (or `show`, `panel`):
|
||||
- **Description:** Display all registered hooks with their status.
|
||||
|
||||
- **`/ide`**
|
||||
- **Description:** Manage IDE integration.
|
||||
- **Sub-commands:**
|
||||
- **`disable`**:
|
||||
- **Description:** Disable IDE integration.
|
||||
- **`enable`**:
|
||||
- **Description:** Enable IDE integration.
|
||||
- **`install`**:
|
||||
- **Description:** Install required IDE companion.
|
||||
- **`status`**:
|
||||
- **Description:** Check status of IDE integration.
|
||||
|
||||
- **`/init`**
|
||||
- **Description:** To help users easily create a `GEMINI.md` file, this
|
||||
command analyzes the current directory and generates a tailored context
|
||||
file, making it simpler for them to provide project-specific instructions to
|
||||
the Gemini agent.
|
||||
|
||||
- **`/introspect`**
|
||||
- **Description:** Provide debugging information about the current Gemini CLI
|
||||
session, including the state of loaded sub-agents and active hooks. This
|
||||
command is primarily for advanced users and developers.
|
||||
|
||||
- **`/mcp`**
|
||||
- **Description:** Manage configured Model Context Protocol (MCP) servers.
|
||||
- **Sub-commands:**
|
||||
- **`list`** or **`ls`**:
|
||||
- **Description:** List configured MCP servers and tools. This is the
|
||||
default action if no subcommand is specified.
|
||||
- **`desc`**
|
||||
- **Description:** List configured MCP servers and tools with
|
||||
descriptions.
|
||||
- **`schema`**:
|
||||
- **Description:** List configured MCP servers and tools with descriptions
|
||||
and schemas.
|
||||
- **`auth`**:
|
||||
- **Description:** Authenticate with an OAuth-enabled MCP server.
|
||||
- **Usage:** `/mcp auth <server-name>`
|
||||
- **Details:** If `<server-name>` is provided, it initiates the OAuth flow
|
||||
for that server. If no server name is provided, it lists all configured
|
||||
servers that support OAuth authentication.
|
||||
- **`desc`**
|
||||
- **Description:** List configured MCP servers and tools with
|
||||
descriptions.
|
||||
- **`list`** or **`ls`**:
|
||||
- **Description:** List configured MCP servers and tools. This is the
|
||||
default action if no subcommand is specified.
|
||||
- **`refresh`**:
|
||||
- **Description:** Restarts all MCP servers and re-discovers their
|
||||
available tools.
|
||||
|
||||
- [**`/model`**](./model.md)
|
||||
- **Description:** Opens a dialog to choose your Gemini model.
|
||||
- **`schema`**:
|
||||
- **Description:** List configured MCP servers and tools with descriptions
|
||||
and schemas.
|
||||
|
||||
- **`/memory`**
|
||||
- **Description:** Manage the AI's instructional context (hierarchical memory
|
||||
@@ -141,23 +184,40 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **`add`**:
|
||||
- **Description:** Adds the following text to the AI's memory. Usage:
|
||||
`/memory add <text to remember>`
|
||||
- **`show`**:
|
||||
- **Description:** Display the full, concatenated content of the current
|
||||
hierarchical memory that has been loaded from all `GEMINI.md` files.
|
||||
This lets you inspect the instructional context being provided to the
|
||||
Gemini model.
|
||||
- **`list`**:
|
||||
- **Description:** Lists the paths of the GEMINI.md files in use for
|
||||
hierarchical memory.
|
||||
- **`refresh`**:
|
||||
- **Description:** Reload the hierarchical instructional memory from all
|
||||
`GEMINI.md` files found in the configured locations (global,
|
||||
project/ancestors, and sub-directories). This command updates the model
|
||||
with the latest `GEMINI.md` content.
|
||||
- **`list`**:
|
||||
- **Description:** Lists the paths of the GEMINI.md files in use for
|
||||
hierarchical memory.
|
||||
- **`show`**:
|
||||
- **Description:** Display the full, concatenated content of the current
|
||||
hierarchical memory that has been loaded from all `GEMINI.md` files.
|
||||
This lets you inspect the instructional context being provided to the
|
||||
Gemini model.
|
||||
- **Note:** For more details on how `GEMINI.md` files contribute to
|
||||
hierarchical memory, see the
|
||||
[CLI Configuration documentation](../get-started/configuration.md).
|
||||
|
||||
- [**`/model`**](./model.md)
|
||||
- **Description:** Opens a dialog to choose your Gemini model.
|
||||
|
||||
- **`/policies`**
|
||||
- **Description:** Manage policies.
|
||||
- **Sub-commands:**
|
||||
- **`list`**:
|
||||
- **Description:** List all active policies grouped by mode.
|
||||
|
||||
- **`/privacy`**
|
||||
- **Description:** Display the Privacy Notice and allow users to select
|
||||
whether they consent to the collection of their data for service improvement
|
||||
purposes.
|
||||
|
||||
- **`/quit`** (or **`/exit`**)
|
||||
- **Description:** Exit Gemini CLI.
|
||||
|
||||
- **`/restore`**
|
||||
- **Description:** Restores the project files to the state they were in just
|
||||
before a tool was executed. This is particularly useful for undoing file
|
||||
@@ -168,18 +228,24 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
[settings](../get-started/configuration.md). See
|
||||
[Checkpointing documentation](../cli/checkpointing.md) for more details.
|
||||
|
||||
- **`/rewind`**
|
||||
- **Description:** Navigates backward through the conversation history,
|
||||
allowing you to review past interactions and potentially revert to a
|
||||
previous state. This feature helps in managing complex or branched
|
||||
conversations.
|
||||
|
||||
- **`/resume`**
|
||||
- **Description:** Browse and resume previous conversation sessions. Opens an
|
||||
interactive session browser where you can search, filter, and select from
|
||||
automatically saved conversations.
|
||||
- **Features:**
|
||||
- **Session Browser:** Interactive interface showing all saved sessions with
|
||||
timestamps, message counts, and first user message for context
|
||||
- **Search:** Use `/` to search through conversation content across all
|
||||
sessions
|
||||
- **Sorting:** Sort sessions by date or message count
|
||||
- **Management:** Delete unwanted sessions directly from the browser
|
||||
- **Resume:** Select any session to resume and continue the conversation
|
||||
- **Search:** Use `/` to search through conversation content across all
|
||||
sessions
|
||||
- **Session Browser:** Interactive interface showing all saved sessions with
|
||||
timestamps, message counts, and first user message for context
|
||||
- **Sorting:** Sort sessions by date or message count
|
||||
- **Note:** All conversations are automatically saved as you chat - no manual
|
||||
saving required. See [Session Management](../cli/session-management.md) for
|
||||
complete details.
|
||||
@@ -198,19 +264,26 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
modify them as desired. Changes to some settings are applied immediately,
|
||||
while others require a restart.
|
||||
|
||||
- **`/shells`** (or **`/bashes`**)
|
||||
- **Description:** Toggle the background shells view. This allows you to view
|
||||
and manage long-running processes that you've sent to the background.
|
||||
- **`/setup-github`**
|
||||
- **Description:** Set up GitHub Actions to triage issues and review PRs with
|
||||
Gemini.
|
||||
|
||||
- [**`/skills`**](./skills.md)
|
||||
- **Description:** Manage Agent Skills, which provide on-demand expertise and
|
||||
specialized workflows.
|
||||
- **Sub-commands:**
|
||||
- **`disable <name>`**:
|
||||
- **Description:** Disable a specific skill by name.
|
||||
- **Usage:** `/skills disable <name>`
|
||||
- **`enable <name>`**:
|
||||
- **Description:** Enable a specific skill by name.
|
||||
- **Usage:** `/skills enable <name>`
|
||||
- **`list`**:
|
||||
- **Description:** List all discovered skills and their current status
|
||||
(enabled/disabled).
|
||||
- **`enable`**:
|
||||
- **Description:** Enable a specific skill by name.
|
||||
- **Usage:** `/skills enable <name>`
|
||||
- **`disable`**:
|
||||
- **Description:** Disable a specific skill by name.
|
||||
- **Usage:** `/skills disable <name>`
|
||||
- **`reload`**:
|
||||
- **Description:** Refresh the list of discovered skills from all tiers
|
||||
(workspace, user, and extensions).
|
||||
@@ -222,18 +295,14 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
cached tokens are being used, which occurs with API key authentication but
|
||||
not with OAuth authentication at this time.
|
||||
|
||||
- **`/terminal-setup`**
|
||||
- **Description:** Configure terminal keybindings for multiline input (VS
|
||||
Code, Cursor, Windsurf).
|
||||
|
||||
- [**`/theme`**](./themes.md)
|
||||
- **Description:** Open a dialog that lets you change the visual theme of
|
||||
Gemini CLI.
|
||||
|
||||
- **`/auth`**
|
||||
- **Description:** Open a dialog that lets you change the authentication
|
||||
method.
|
||||
|
||||
- **`/about`**
|
||||
- **Description:** Show version info. Please share this information when
|
||||
filing issues.
|
||||
|
||||
- [**`/tools`**](../tools/index.md)
|
||||
- **Description:** Display a list of tools that are currently available within
|
||||
Gemini CLI.
|
||||
@@ -245,37 +314,23 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **`nodesc`** or **`nodescriptions`**:
|
||||
- **Description:** Hide tool descriptions, showing only the tool names.
|
||||
|
||||
- **`/privacy`**
|
||||
- **Description:** Display the Privacy Notice and allow users to select
|
||||
whether they consent to the collection of their data for service improvement
|
||||
purposes.
|
||||
|
||||
- **`/quit`** (or **`/exit`**)
|
||||
- **Description:** Exit Gemini CLI.
|
||||
|
||||
- **`/vim`**
|
||||
- **Description:** Toggle vim mode on or off. When vim mode is enabled, the
|
||||
input area supports vim-style navigation and editing commands in both NORMAL
|
||||
and INSERT modes.
|
||||
- **Features:**
|
||||
- **Count support:** Prefix commands with numbers (e.g., `3h`, `5w`, `10G`)
|
||||
- **Editing commands:** Delete with `x`, change with `c`, insert with `i`,
|
||||
`a`, `o`, `O`; complex operations like `dd`, `cc`, `dw`, `cw`
|
||||
- **INSERT mode:** Standard text input with escape to return to NORMAL mode
|
||||
- **NORMAL mode:** Navigate with `h`, `j`, `k`, `l`; jump by words with `w`,
|
||||
`b`, `e`; go to line start/end with `0`, `$`, `^`; go to specific lines
|
||||
with `G` (or `gg` for first line)
|
||||
- **INSERT mode:** Standard text input with escape to return to NORMAL mode
|
||||
- **Editing commands:** Delete with `x`, change with `c`, insert with `i`,
|
||||
`a`, `o`, `O`; complex operations like `dd`, `cc`, `dw`, `cw`
|
||||
- **Count support:** Prefix commands with numbers (e.g., `3h`, `5w`, `10G`)
|
||||
- **Repeat last command:** Use `.` to repeat the last editing operation
|
||||
- **Persistent setting:** Vim mode preference is saved to
|
||||
`~/.gemini/settings.json` and restored between sessions
|
||||
- **Status indicator:** When enabled, shows `[NORMAL]` or `[INSERT]` in the
|
||||
footer
|
||||
|
||||
- **`/init`**
|
||||
- **Description:** To help users easily create a `GEMINI.md` file, this
|
||||
command analyzes the current directory and generates a tailored context
|
||||
file, making it simpler for them to provide project-specific instructions to
|
||||
the Gemini agent.
|
||||
- **Repeat last command:** Use `.` to repeat the last editing operation
|
||||
- **Status indicator:** When enabled, shows `[NORMAL]` or `[INSERT]` in the
|
||||
footer
|
||||
|
||||
### Custom commands
|
||||
|
||||
@@ -288,12 +343,12 @@ please see the dedicated [Custom Commands documentation](./custom-commands.md).
|
||||
These shortcuts apply directly to the input prompt for text manipulation.
|
||||
|
||||
- **Undo:**
|
||||
- **Keyboard shortcut:** Press **Ctrl+z** to undo the last action in the input
|
||||
prompt.
|
||||
- **Keyboard shortcut:** Press **Cmd+z** or **Alt+z** to undo the last action
|
||||
in the input prompt.
|
||||
|
||||
- **Redo:**
|
||||
- **Keyboard shortcut:** Press **Ctrl+Shift+Z** to redo the last undone action
|
||||
in the input prompt.
|
||||
- **Keyboard shortcut:** Press **Shift+Cmd+Z** or **Shift+Alt+Z** to redo the
|
||||
last undone action in the input prompt.
|
||||
|
||||
## At commands (`@`)
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Creating Agent Skills
|
||||
|
||||
This guide provides an overview of how to create your own Agent Skills to extend
|
||||
the capabilities of Gemini CLI.
|
||||
|
||||
## Getting started: The `skill-creator` skill
|
||||
|
||||
The recommended way to create a new skill is to use the built-in `skill-creator`
|
||||
skill. To use it, ask Gemini CLI to create a new skill for you.
|
||||
|
||||
**Example prompt:**
|
||||
|
||||
> "create a new skill called 'code-reviewer'"
|
||||
|
||||
Gemini CLI will then use the `skill-creator` to generate the skill:
|
||||
|
||||
1. Generate a new directory for your skill (e.g., `my-new-skill/`).
|
||||
2. Create a `SKILL.md` file with the necessary YAML frontmatter (`name` and
|
||||
`description`).
|
||||
3. Create the standard resource directories: `scripts/`, `references/`, and
|
||||
`assets/`.
|
||||
|
||||
## Manual skill creation
|
||||
|
||||
If you prefer to create skills manually:
|
||||
|
||||
1. **Create a directory** for your skill (e.g., `my-new-skill/`).
|
||||
2. **Create a `SKILL.md` file** inside the new directory.
|
||||
|
||||
To add additional resources that support the skill, refer to the skill
|
||||
structure.
|
||||
|
||||
## Skill structure
|
||||
|
||||
A skill is a directory containing a `SKILL.md` file at its root.
|
||||
|
||||
### Folder structure
|
||||
|
||||
While a `SKILL.md` file is the only required component, we recommend the
|
||||
following structure for organizing your skill's resources:
|
||||
|
||||
```text
|
||||
my-skill/
|
||||
├── SKILL.md (Required) Instructions and metadata
|
||||
├── scripts/ (Optional) Executable scripts
|
||||
├── references/ (Optional) Static documentation
|
||||
└── assets/ (Optional) Templates and other resources
|
||||
```
|
||||
|
||||
### `SKILL.md` file
|
||||
|
||||
The `SKILL.md` file is the core of your skill. This file uses YAML frontmatter
|
||||
for metadata and Markdown for instructions. For example:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-reviewer
|
||||
description:
|
||||
Use this skill to review code. It supports both local changes and remote Pull
|
||||
Requests.
|
||||
---
|
||||
|
||||
# Code Reviewer
|
||||
|
||||
This skill guides the agent in conducting thorough code reviews.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Determine Review Target
|
||||
|
||||
- **Remote PR**: If the user gives a PR number or URL, target that remote PR.
|
||||
- **Local Changes**: If changes are local... ...
|
||||
```
|
||||
|
||||
- **`name`**: A unique identifier for the skill. This should match the directory
|
||||
name.
|
||||
- **`description`**: A description of what the skill does and when Gemini should
|
||||
use it.
|
||||
- **Body**: The Markdown body of the file contains the instructions that guide
|
||||
the agent's behavior when the skill is active.
|
||||
@@ -23,24 +23,24 @@ available combinations.
|
||||
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End (no Shift, Ctrl)` |
|
||||
| Move the cursor up one line. | `Up Arrow (no Shift, Alt, Ctrl, Cmd)` |
|
||||
| Move the cursor down one line. | `Down Arrow (no Shift, Alt, Ctrl, Cmd)` |
|
||||
| Move the cursor one character to the left. | `Left Arrow (no Shift, Alt, Ctrl, Cmd)`<br />`Ctrl + B` |
|
||||
| Move the cursor one character to the left. | `Left Arrow (no Shift, Alt, Ctrl, Cmd)` |
|
||||
| Move the cursor one character to the right. | `Right Arrow (no Shift, Alt, Ctrl, Cmd)`<br />`Ctrl + F` |
|
||||
| Move the cursor one word to the left. | `Ctrl + Left Arrow`<br />`Alt + Left Arrow`<br />`Alt + B` |
|
||||
| Move the cursor one word to the right. | `Ctrl + Right Arrow`<br />`Alt + Right Arrow`<br />`Alt + F` |
|
||||
|
||||
#### Editing
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------------------------------ | --------------------------------------------------------- |
|
||||
| Delete from the cursor to the end of the line. | `Ctrl + K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl + U` |
|
||||
| Clear all text in the input field. | `Ctrl + C` |
|
||||
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
|
||||
| Undo the most recent text edit. | `Ctrl + Z (no Shift)` |
|
||||
| Redo the most recent undone text edit. | `Shift + Ctrl + Z` |
|
||||
| Action | Keys |
|
||||
| ------------------------------------------------ | ---------------------------------------------------------------- |
|
||||
| Delete from the cursor to the end of the line. | `Ctrl + K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl + U` |
|
||||
| Clear all text in the input field. | `Ctrl + C` |
|
||||
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
|
||||
| Undo the most recent text edit. | `Cmd + Z (no Shift)`<br />`Alt + Z (no Shift)` |
|
||||
| Redo the most recent undone text edit. | `Shift + Ctrl + Z`<br />`Shift + Cmd + Z`<br />`Shift + Alt + Z` |
|
||||
|
||||
#### Scrolling
|
||||
|
||||
@@ -106,10 +106,19 @@ available combinations.
|
||||
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
|
||||
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` |
|
||||
| Expand a height-constrained response to show additional lines when not in alternate buffer mode. | `Ctrl + O`<br />`Ctrl + S` |
|
||||
| Ctrl+B | `Ctrl + B` |
|
||||
| Ctrl+L | `Ctrl + L` |
|
||||
| Ctrl+K | `Ctrl + K` |
|
||||
| Enter | `Enter` |
|
||||
| Esc | `Esc` |
|
||||
| Shift+Tab | `Shift + Tab` |
|
||||
| Tab | `Tab (no Shift)` |
|
||||
| Tab | `Tab (no Shift)` |
|
||||
| Focus the shell input from the gemini input. | `Tab (no Shift)` |
|
||||
| Focus the Gemini input from the shell input. | `Tab` |
|
||||
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
|
||||
| Restart the application. | `R` |
|
||||
| Suspend the application (not yet implemented). | `Ctrl + Z` |
|
||||
|
||||
<!-- KEYBINDINGS-AUTOGEN:END -->
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Rewind
|
||||
|
||||
The `/rewind` command allows you to go back to a previous state in your
|
||||
conversation and, optionally, revert any file changes made by the AI during
|
||||
those interactions. This is a powerful tool for undoing mistakes, exploring
|
||||
different approaches, or simply cleaning up your session history.
|
||||
|
||||
## Usage
|
||||
|
||||
To use the rewind feature, simply type `/rewind` into the input prompt and press
|
||||
**Enter**.
|
||||
|
||||
Alternatively, you can use the keyboard shortcut: **Press `Esc` twice**.
|
||||
|
||||
## Interface
|
||||
|
||||
When you trigger a rewind, an interactive list of your previous interactions
|
||||
appears.
|
||||
|
||||
1. **Select Interaction:** Use the **Up/Down arrow keys** to navigate through
|
||||
the list. The most recent interactions are at the bottom.
|
||||
2. **Preview:** As you select an interaction, you'll see a preview of the user
|
||||
prompt and, if applicable, the number of files changed during that step.
|
||||
3. **Confirm Selection:** Press **Enter** on the interaction you want to rewind
|
||||
back to.
|
||||
4. **Action Selection:** After selecting an interaction, you'll be presented
|
||||
with a confirmation dialog with up to three options:
|
||||
- **Rewind conversation and revert code changes:** Reverts both the chat
|
||||
history and the file modifications to the state before the selected
|
||||
interaction.
|
||||
- **Rewind conversation:** Only reverts the chat history. File changes are
|
||||
kept.
|
||||
- **Revert code changes:** Only reverts the file modifications. The chat
|
||||
history is kept.
|
||||
- **Do nothing (esc):** Cancels the rewind operation.
|
||||
|
||||
If no code changes were made since the selected point, the options related to
|
||||
reverting code changes will be hidden.
|
||||
|
||||
## Key Considerations
|
||||
|
||||
- **Destructive Action:** Rewinding is a destructive action for your current
|
||||
session history and potentially your files. Use it with care.
|
||||
- **Agent Awareness:** When you rewind the conversation, the AI model loses all
|
||||
memory of the interactions that were removed. If you only revert code changes,
|
||||
you may need to inform the model that the files have changed.
|
||||
- **Manual Edits:** Rewinding only affects file changes made by the AI's edit
|
||||
tools. It does **not** undo manual edits you've made or changes triggered by
|
||||
the shell tool (`!`).
|
||||
- **Compression:** Rewind works across chat compression points by reconstructing
|
||||
the history from stored session data.
|
||||
+14
-11
@@ -57,6 +57,7 @@ they appear in the UI.
|
||||
| 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 logged-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` |
|
||||
@@ -76,6 +77,7 @@ they appear in the UI.
|
||||
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ------- |
|
||||
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
|
||||
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
|
||||
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
|
||||
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
|
||||
|
||||
### Context
|
||||
@@ -96,7 +98,6 @@ they appear in the UI.
|
||||
| -------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
|
||||
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
|
||||
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
|
||||
| Auto Accept | `tools.autoAccept` | Automatically accept and execute tool calls that are considered safe (e.g., read-only operations). | `false` |
|
||||
| Approval Mode | `tools.approvalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
|
||||
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
|
||||
| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` |
|
||||
@@ -106,13 +107,14 @@ they appear in the UI.
|
||||
|
||||
### Security
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | ------- |
|
||||
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
||||
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
|
||||
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
|
||||
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
|
||||
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
||||
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
|
||||
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
|
||||
| Extension Source Regex Allowlist | `security.allowedExtensions` | List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting. | `[]` |
|
||||
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
|
||||
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
|
||||
|
||||
### Experimental
|
||||
|
||||
@@ -129,8 +131,9 @@ they appear in the UI.
|
||||
|
||||
### HooksConfig
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------ | --------------------------- | ------------------------------------------------ | ------- |
|
||||
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------ | --------------------------- | -------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Hooks | `hooksConfig.enabled` | Canonical toggle for the hooks system. When disabled, no hooks will be executed. | `true` |
|
||||
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
|
||||
|
||||
<!-- SETTINGS-AUTOGEN:END -->
|
||||
|
||||
+5
-77
@@ -89,83 +89,6 @@ gemini skills enable my-expertise
|
||||
gemini skills disable my-expertise --scope workspace
|
||||
```
|
||||
|
||||
## Creating a Skill
|
||||
|
||||
A skill is a directory containing a `SKILL.md` file at its root. This file uses
|
||||
YAML frontmatter for metadata and Markdown for instructions.
|
||||
|
||||
### Folder Structure
|
||||
|
||||
Skills are self-contained directories. At a minimum, a skill requires a
|
||||
`SKILL.md` file, but can include other resources:
|
||||
|
||||
```text
|
||||
my-skill/
|
||||
├── SKILL.md (Required) Instructions and metadata
|
||||
├── scripts/ (Optional) Executable scripts/tools
|
||||
├── references/ (Optional) Static documentation and examples
|
||||
└── assets/ (Optional) Templates and binary resources
|
||||
```
|
||||
|
||||
### Basic Structure (SKILL.md)
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: <unique-name>
|
||||
description: <what the skill does and when Gemini should use it>
|
||||
---
|
||||
|
||||
<your instructions for how the agent should behave / use the skill>
|
||||
```
|
||||
|
||||
- **`name`**: A unique identifier (lowercase, alphanumeric, and dashes).
|
||||
- **`description`**: The most critical field. Gemini uses this to decide when
|
||||
the skill is relevant. Be specific about the expertise provided.
|
||||
- **Body**: Everything below the second `---` is injected as expert procedural
|
||||
guidance for the model.
|
||||
|
||||
### Example: Team Code Reviewer
|
||||
|
||||
Create `~/.gemini/skills/code-reviewer/SKILL.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-reviewer
|
||||
description:
|
||||
Expertise in reviewing code for style, security, and performance. Use when the
|
||||
user asks for "feedback," a "review," or to "check" their changes.
|
||||
---
|
||||
|
||||
# Code Reviewer
|
||||
|
||||
You are an expert code reviewer. When reviewing code, follow this workflow:
|
||||
|
||||
1. **Analyze**: Review the staged changes or specific files provided. Ensure
|
||||
that the changes are scoped properly and represent minimal changes required
|
||||
to address the issue.
|
||||
2. **Style**: Ensure code follows the workspace's conventions and idiomatic
|
||||
patterns as described in the `GEMINI.md` file.
|
||||
3. **Security**: Flag any potential security vulnerabilities.
|
||||
4. **Tests**: Verify that new logic has corresponding test coverage and that
|
||||
the test coverage adequately validates the changes.
|
||||
|
||||
Provide your feedback as a concise bulleted list of "Strengths" and
|
||||
"Opportunities."
|
||||
```
|
||||
|
||||
### Resource Conventions
|
||||
|
||||
While you can structure your skill directory however you like, the Agent Skills
|
||||
standard encourages these conventions:
|
||||
|
||||
- **`scripts/`**: Executable scripts (bash, python, node) the agent can run.
|
||||
- **`references/`**: Static documentation, schemas, or example data for the
|
||||
agent to consult.
|
||||
- **`assets/`**: Code templates, boilerplate, or binary resources.
|
||||
|
||||
When a skill is activated, Gemini CLI provides the model with a tree view of the
|
||||
entire skill directory, allowing it to discover and utilize these assets.
|
||||
|
||||
## How it Works (Security & Privacy)
|
||||
|
||||
1. **Discovery**: At the start of a session, Gemini CLI scans the discovery
|
||||
@@ -182,3 +105,8 @@ entire skill directory, allowing it to discover and utilize these assets.
|
||||
it permission to read any bundled assets.
|
||||
5. **Execution**: The model proceeds with the specialized expertise active. It
|
||||
is instructed to prioritize the skill's procedural guidance within reason.
|
||||
|
||||
## Creating your own skills
|
||||
|
||||
To create your own skills, see the [Create Agent Skills](./creating-skills.md)
|
||||
guide.
|
||||
|
||||
@@ -210,6 +210,10 @@ decision = "ask_user"
|
||||
# The priority of the rule, from 0 to 999.
|
||||
priority = 10
|
||||
|
||||
# (Optional) A custom message to display when a tool call is denied by this rule.
|
||||
# This message is returned to the model and user, useful for explaining *why* it was denied.
|
||||
deny_message = "Deletion is permanent"
|
||||
|
||||
# (Optional) An array of approval modes where this rule is active.
|
||||
modes = ["autoEdit"]
|
||||
```
|
||||
@@ -282,6 +286,7 @@ only the `mcpName`.
|
||||
mcpName = "untrusted-server"
|
||||
decision = "deny"
|
||||
priority = 500
|
||||
deny_message = "This server is not trusted by the admin."
|
||||
```
|
||||
|
||||
## Default policies
|
||||
@@ -290,9 +295,9 @@ The Gemini CLI ships with a set of default policies to provide a safe
|
||||
out-of-the-box experience.
|
||||
|
||||
- **Read-only tools** (like `read_file`, `glob`) are generally **allowed**.
|
||||
- **Agent delegation** (like `delegate_to_agent`) defaults to **`ask_user`** to
|
||||
ensure remote agents can prompt for confirmation, but local sub-agent actions
|
||||
are executed silently and checked individually.
|
||||
- **Agent delegation** defaults to **`ask_user`** to ensure remote agents can
|
||||
prompt for confirmation, but local sub-agent actions are executed silently and
|
||||
checked individually.
|
||||
- **Write tools** (like `write_file`, `run_shell_command`) default to
|
||||
**`ask_user`**.
|
||||
- In **`yolo`** mode, a high-priority rule allows all tools.
|
||||
|
||||
+34
-29
@@ -24,30 +24,29 @@ the main agent's context or toolset.
|
||||
|
||||
## What are sub-agents?
|
||||
|
||||
Think of sub-agents as "specialists" that the main Gemini agent can hire for a
|
||||
specific job.
|
||||
Sub-agents are "specialists" that the main Gemini agent can hire for a specific
|
||||
job.
|
||||
|
||||
- **Focused context:** Each sub-agent has its own system prompt and persona.
|
||||
- **Specialized tools:** Sub-agents can have a restricted or specialized set of
|
||||
tools.
|
||||
- **Independent context window:** Interactions with a sub-agent happen in a
|
||||
separate context loop. The main agent only sees the final result, saving
|
||||
tokens in your main conversation history.
|
||||
separate context loop, which saves tokens in your main conversation history.
|
||||
|
||||
Sub-agents are exposed to the main agent as a tool of the same name which
|
||||
delegates to the sub-agent, when called. Once the sub-agent completes its task
|
||||
(or fails), it reports back to the main agent with its findings (usually as a
|
||||
text summary or structured report returned by the tool).
|
||||
Sub-agents are exposed to the main agent as a tool of the same name. When the
|
||||
main agent calls the tool, it delegates the task to the sub-agent. Once the
|
||||
sub-agent completes its task, it reports back to the main agent with its
|
||||
findings.
|
||||
|
||||
## Built-in sub-agents
|
||||
|
||||
Gemini CLI comes with powerful built-in sub-agents.
|
||||
Gemini CLI comes with the following built-in sub-agents:
|
||||
|
||||
### Codebase Investigator
|
||||
|
||||
- **Name:** `codebase_investigator`
|
||||
- **Purpose:** Deep analysis of the codebase, reverse engineering, and
|
||||
understanding complex dependencies.
|
||||
- **Purpose:** Analyze the codebase, reverse engineer, and understand complex
|
||||
dependencies.
|
||||
- **When to use:** "How does the authentication system work?", "Map out the
|
||||
dependencies of the `AgentRegistry` class."
|
||||
- **Configuration:** Enabled by default. You can configure it in
|
||||
@@ -67,20 +66,25 @@ Gemini CLI comes with powerful built-in sub-agents.
|
||||
### CLI Help Agent
|
||||
|
||||
- **Name:** `cli_help`
|
||||
- **Purpose:** Expert knowledge about Gemini CLI itself, its commands,
|
||||
- **Purpose:** Get expert knowledge about Gemini CLI itself, its commands,
|
||||
configuration, and documentation.
|
||||
- **When to use:** "How do I configure a proxy?", "What does the `/rewind`
|
||||
command do?"
|
||||
- **Configuration:** Enabled by default.
|
||||
|
||||
### Generalist Agent
|
||||
|
||||
- **Name:** `generalist_agent`
|
||||
- **Purpose:** Route tasks to the appropriate specialized sub-agent.
|
||||
- **When to use:** Implicitly used by the main agent for routing. Not directly
|
||||
invoked by the user.
|
||||
- **Configuration:** Enabled by default. No specific configuration options.
|
||||
|
||||
## Creating custom sub-agents
|
||||
|
||||
You can create your own sub-agents to automate specific workflows or enforce
|
||||
specific personas.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
To use custom sub-agents, you must enable them in your `settings.json`:
|
||||
specific personas. To use custom sub-agents, you must enable them in your
|
||||
`settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -123,10 +127,10 @@ vulnerabilities.
|
||||
|
||||
Focus on:
|
||||
|
||||
1. SQL Injection
|
||||
2. XSS (Cross-Site Scripting)
|
||||
3. Hardcoded credentials
|
||||
4. Unsafe file operations
|
||||
1. SQL Injection
|
||||
2. XSS (Cross-Site Scripting)
|
||||
3. Hardcoded credentials
|
||||
4. Unsafe file operations
|
||||
|
||||
When you find a vulnerability, explain it clearly and suggest a fix. Do not fix
|
||||
it yourself; just report it.
|
||||
@@ -147,17 +151,16 @@ it yourself; just report it.
|
||||
|
||||
### Optimizing your sub-agent
|
||||
|
||||
The main agent system prompt contains language that encourages use of an expert
|
||||
sub-agent when one is available for the task at hand. It decides whether an
|
||||
agent is a relevant expert based on the agent's description. You can improve the
|
||||
reliability with which an agent is used by updating the description to more
|
||||
clearly indicate:
|
||||
The main agent's system prompt encourages it to use an expert sub-agent when one
|
||||
is available. It decides whether an agent is a relevant expert based on the
|
||||
agent's description. You can improve the reliability with which an agent is used
|
||||
by updating the description to more clearly indicate:
|
||||
|
||||
- Its area of expertise.
|
||||
- When it should be used.
|
||||
- Some example scenarios.
|
||||
|
||||
For example: the following sub-agent description should be called fairly
|
||||
For example, the following sub-agent description should be called fairly
|
||||
consistently for Git operations.
|
||||
|
||||
> Git expert agent which should be used for all local and remote git operations.
|
||||
@@ -171,11 +174,13 @@ If you need to further tune your sub-agent, 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 sub-agent 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
|
||||
Gemini CLI can also delegate tasks to remote sub-agents using the Agent-to-Agent
|
||||
(A2A) protocol.
|
||||
|
||||
> **Note: Remote subagents are currently an experimental feature.**
|
||||
|
||||
See the [Remote Subagents documentation](/docs/core/remote-agents) for detailed
|
||||
configuration and usage instructions.
|
||||
|
||||
|
||||
@@ -215,14 +215,6 @@ a few things you can try in order of recommendation:
|
||||
MCP servers of their own. This should not be used as an airtight security
|
||||
mechanism.
|
||||
|
||||
- **`autoAccept`** (boolean):
|
||||
- **Description:** Controls whether the CLI automatically accepts and executes
|
||||
tool calls that are considered safe (e.g., read-only operations) without
|
||||
explicit user confirmation. If set to `true`, the CLI will bypass the
|
||||
confirmation prompt for tools deemed safe.
|
||||
- **Default:** `false`
|
||||
- **Example:** `"autoAccept": true`
|
||||
|
||||
- **`theme`** (string):
|
||||
- **Description:** Sets the visual [theme](../cli/themes.md) for Gemini CLI.
|
||||
- **Default:** `"Default"`
|
||||
|
||||
@@ -244,6 +244,10 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Show the model name in the chat for each model turn.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.showUserIdentity`** (boolean):
|
||||
- **Description:** Show the logged-in user's identity (e.g. email) in the UI.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`ui.useAlternateBuffer`** (boolean):
|
||||
- **Description:** Use an alternate screen buffer for the UI, preserving shell
|
||||
history.
|
||||
@@ -322,6 +326,12 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `0.5`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`model.disableLoopDetection`** (boolean):
|
||||
- **Description:** Disable automatic detection and prevention of infinite
|
||||
loops.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`model.skipNextSpeakerCheck`** (boolean):
|
||||
- **Description:** Skip the next speaker check.
|
||||
- **Default:** `true`
|
||||
@@ -657,11 +667,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
performance.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`tools.autoAccept`** (boolean):
|
||||
- **Description:** Automatically accept and execute tool calls that are
|
||||
considered safe (e.g., read-only operations).
|
||||
- **Default:** `false`
|
||||
|
||||
- **`tools.approvalMode`** (enum):
|
||||
- **Description:** The default approval mode for tool execution. 'default'
|
||||
prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is
|
||||
@@ -729,12 +734,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`tools.enableHooks`** (boolean):
|
||||
- **Description:** Enables the hooks system experiment. When disabled, the
|
||||
hooks system is completely deactivated regardless of other settings.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `mcp`
|
||||
|
||||
- **`mcp.serverCommand`** (string):
|
||||
@@ -775,6 +774,13 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`security.allowedExtensions`** (array):
|
||||
- **Description:** List of Regex patterns for allowed extensions. If nonempty,
|
||||
only extensions that match the patterns in this list are allowed. Overrides
|
||||
the blockGitExtensions setting.
|
||||
- **Default:** `[]`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`security.folderTrust.enabled`** (boolean):
|
||||
- **Description:** Setting to track whether Folder trust is enabled.
|
||||
- **Default:** `false`
|
||||
@@ -869,11 +875,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.skills`** (boolean):
|
||||
- **Description:** [Deprecated] Enable Agent Skills (experimental).
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.useOSC52Paste`** (boolean):
|
||||
- **Description:** Use OSC 52 sequence for pasting instead of clipboardy
|
||||
(useful for remote sessions).
|
||||
@@ -902,6 +903,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Canonical toggle for the hooks system. When disabled, no
|
||||
hooks will be executed.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`hooksConfig.disabled`** (array):
|
||||
- **Description:** List of hook names (commands) that should be disabled.
|
||||
@@ -1185,6 +1187,10 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
- **Description:** The path to your Google Application Credentials JSON file.
|
||||
- **Example:**
|
||||
`export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/credentials.json"`
|
||||
- **`GOOGLE_GENAI_API_VERSION`**:
|
||||
- Specifies the API version to use for Gemini API requests.
|
||||
- When set, overrides the default API version used by the SDK.
|
||||
- Example: `export GOOGLE_GENAI_API_VERSION="v1"`
|
||||
- **`OTLP_GOOGLE_CLOUD_PROJECT`**:
|
||||
- Your Google Cloud Project ID for Telemetry in Google Cloud
|
||||
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
|
||||
|
||||
@@ -167,8 +167,8 @@ case is response validation and automatic retries.
|
||||
- `reason`: Required if denied. This text is sent **to the agent as a new
|
||||
prompt** to request a correction.
|
||||
- `continue`: Set to `false` to **stop the session** without retrying.
|
||||
- `clearContext`: If `true`, clears conversation history (LLM memory) while
|
||||
preserving UI display.
|
||||
- `hookSpecificOutput.clearContext`: If `true`, clears conversation history
|
||||
(LLM memory) while preserving UI display.
|
||||
- **Exit Code 2 (Retry)**: Rejects the response and triggers an automatic retry
|
||||
turn using `stderr` as the feedback prompt.
|
||||
|
||||
|
||||
+99
-125
@@ -1,149 +1,123 @@
|
||||
# Welcome to Gemini CLI documentation
|
||||
# Gemini CLI documentation
|
||||
|
||||
This documentation provides a comprehensive guide to installing, using, and
|
||||
developing Gemini CLI, a tool that lets you interact with Gemini models through
|
||||
a command-line interface.
|
||||
Gemini CLI is an open-source AI agent that brings the power of Gemini directly
|
||||
into your terminal. It is designed to be a terminal-first, extensible, and
|
||||
powerful tool for developers, engineers, SREs, and beyond.
|
||||
|
||||
## Gemini CLI overview
|
||||
Gemini CLI integrates with your local environment. It can read and edit files,
|
||||
execute shell commands, and search the web, all while maintaining your project
|
||||
context.
|
||||
|
||||
Gemini CLI brings the capabilities of Gemini models to your terminal in an
|
||||
interactive Read-Eval-Print Loop (REPL) environment. Gemini CLI consists of a
|
||||
client-side application (`packages/cli`) that communicates with a local server
|
||||
(`packages/core`), which in turn manages requests to the Gemini API and its AI
|
||||
models. Gemini CLI also contains a variety of tools for tasks such as performing
|
||||
file system operations, running shells, and web fetching, which are managed by
|
||||
`packages/core`.
|
||||
## Get started
|
||||
|
||||
## Navigating the documentation
|
||||
Begin your journey with Gemini CLI by setting up your environment and learning
|
||||
the basics.
|
||||
|
||||
This documentation is organized into the following sections:
|
||||
- **[Quickstart](./get-started/index.md):** A streamlined guide to get you
|
||||
chatting in minutes.
|
||||
- **[Installation](./get-started/installation.md):** Instructions for macOS,
|
||||
Linux, and Windows.
|
||||
- **[Authentication](./get-started/authentication.md):** Set up access using
|
||||
Google OAuth, API keys, or Vertex AI.
|
||||
- **[Examples](./get-started/examples.md):** View common usage scenarios to
|
||||
inspire your own workflows.
|
||||
|
||||
### Overview
|
||||
## Use Gemini CLI
|
||||
|
||||
- **[Architecture overview](./architecture.md):** Understand the high-level
|
||||
design of Gemini CLI, including its components and how they interact.
|
||||
- **[Contribution guide](../CONTRIBUTING.md):** Information for contributors and
|
||||
developers, including setup, building, testing, and coding conventions.
|
||||
Master the core capabilities that let Gemini CLI interact with your system
|
||||
safely and effectively.
|
||||
|
||||
### Get started
|
||||
|
||||
- **[Gemini CLI quickstart](./get-started/index.md):** Let's get started with
|
||||
Gemini CLI.
|
||||
- **[Gemini 3 Pro on Gemini CLI](./get-started/gemini-3.md):** Learn how to
|
||||
enable and use Gemini 3.
|
||||
- **[Authentication](./get-started/authentication.md):** Authenticate to Gemini
|
||||
CLI.
|
||||
- **[Configuration](./get-started/configuration.md):** Learn how to configure
|
||||
the CLI.
|
||||
- **[Installation](./get-started/installation.md):** Install and run Gemini CLI.
|
||||
- **[Examples](./get-started/examples.md):** Example usage of Gemini CLI.
|
||||
|
||||
### CLI
|
||||
|
||||
- **[Introduction: Gemini CLI](./cli/index.md):** Overview of the command-line
|
||||
- **[Using the CLI](./cli/index.md):** Learn the basics of the command-line
|
||||
interface.
|
||||
- **[Commands](./cli/commands.md):** Description of available CLI commands.
|
||||
- **[Checkpointing](./cli/checkpointing.md):** Documentation for the
|
||||
checkpointing feature.
|
||||
- **[Custom commands](./cli/custom-commands.md):** Create your own commands and
|
||||
shortcuts for frequently used prompts.
|
||||
- **[Enterprise](./cli/enterprise.md):** Gemini CLI for enterprise.
|
||||
- **[Headless mode](./cli/headless.md):** Use Gemini CLI programmatically for
|
||||
scripting and automation.
|
||||
- **[Keyboard shortcuts](./cli/keyboard-shortcuts.md):** A reference for all
|
||||
keyboard shortcuts to improve your workflow.
|
||||
- **[Model selection](./cli/model.md):** Select the model used to process your
|
||||
commands with `/model`.
|
||||
- **[Sandbox](./cli/sandbox.md):** Isolate tool execution in a secure,
|
||||
containerized environment.
|
||||
- **[Agent Skills](./cli/skills.md):** Extend the CLI with specialized expertise
|
||||
and procedural workflows.
|
||||
- **[Settings](./cli/settings.md):** Configure various aspects of the CLI's
|
||||
behavior and appearance with `/settings`.
|
||||
- **[Telemetry](./cli/telemetry.md):** Overview of telemetry in the CLI.
|
||||
- **[Themes](./cli/themes.md):** Themes for Gemini CLI.
|
||||
- **[Token caching](./cli/token-caching.md):** Token caching and optimization.
|
||||
- **[Trusted Folders](./cli/trusted-folders.md):** An overview of the Trusted
|
||||
Folders security feature.
|
||||
- **[Tutorials](./cli/tutorials.md):** Tutorials for Gemini CLI.
|
||||
- **[Uninstall](./cli/uninstall.md):** Methods for uninstalling the Gemini CLI.
|
||||
- **[File management](./tools/file-system.md):** Grant the model the ability to
|
||||
read code and apply changes directly to your files.
|
||||
- **[Shell commands](./tools/shell.md):** Allow the model to run builds, tests,
|
||||
and git commands.
|
||||
- **[Memory management](./tools/memory.md):** Teach Gemini CLI facts about your
|
||||
project and preferences that persist across sessions.
|
||||
- **[Project context](./cli/gemini-md.md):** Use `GEMINI.md` files to provide
|
||||
persistent context for your projects.
|
||||
- **[Web search and fetch](./tools/web-search.md):** Enable the model to fetch
|
||||
real-time information from the internet.
|
||||
- **[Session management](./cli/session-management.md):** Save, resume, and
|
||||
organize your chat sessions.
|
||||
|
||||
### Core
|
||||
## Configuration
|
||||
|
||||
- **[Introduction: Gemini CLI core](./core/index.md):** Information about Gemini
|
||||
CLI core.
|
||||
- **[Memport](./core/memport.md):** Using the Memory Import Processor.
|
||||
- **[Tools API](./core/tools-api.md):** Information on how the core manages and
|
||||
exposes tools.
|
||||
- **[System Prompt Override](./cli/system-prompt.md):** Replace built-in system
|
||||
instructions using `GEMINI_SYSTEM_MD`.
|
||||
Customize Gemini CLI to match your workflow and preferences.
|
||||
|
||||
- **[Policy Engine](./core/policy-engine.md):** Use the Policy Engine for
|
||||
fine-grained control over tool execution.
|
||||
- **[Settings](./cli/settings.md):** Control response creativity, output
|
||||
verbosity, and more.
|
||||
- **[Model selection](./cli/model.md):** Choose the best Gemini model for your
|
||||
specific task.
|
||||
- **[Ignore files](./cli/gemini-ignore.md):** Use `.geminiignore` to keep
|
||||
sensitive files out of the model's context.
|
||||
- **[Trusted folders](./cli/trusted-folders.md):** Define security boundaries
|
||||
for file access and execution.
|
||||
- **[Token caching](./cli/token-caching.md):** Optimize performance and cost by
|
||||
caching context.
|
||||
- **[Themes](./cli/themes.md):** Personalize the visual appearance of the CLI.
|
||||
|
||||
### Tools
|
||||
## Advanced features
|
||||
|
||||
- **[Introduction: Gemini CLI tools](./tools/index.md):** Information about
|
||||
Gemini CLI's tools.
|
||||
- **[File system tools](./tools/file-system.md):** Documentation for the
|
||||
`read_file` and `write_file` tools.
|
||||
- **[Shell tool](./tools/shell.md):** Documentation for the `run_shell_command`
|
||||
tool.
|
||||
- **[Web fetch tool](./tools/web-fetch.md):** Documentation for the `web_fetch`
|
||||
tool.
|
||||
- **[Web search tool](./tools/web-search.md):** Documentation for the
|
||||
`google_web_search` tool.
|
||||
- **[Memory tool](./tools/memory.md):** Documentation for the `save_memory`
|
||||
tool.
|
||||
- **[Todo tool](./tools/todos.md):** Documentation for the `write_todos` tool.
|
||||
- **[MCP servers](./tools/mcp-server.md):** Using MCP servers with Gemini CLI.
|
||||
Explore powerful features for complex workflows and enterprise environments.
|
||||
|
||||
### Extensions
|
||||
- **[Headless mode](./cli/headless.md):** Run Gemini CLI in scripts or CI/CD
|
||||
pipelines for automated reasoning.
|
||||
- **[Sandboxing](./cli/sandbox.md):** Execute untrusted code or tools in a
|
||||
secure, isolated container.
|
||||
- **[Checkpointing](./cli/checkpointing.md):** Save and restore workspace state
|
||||
to recover from experimental changes.
|
||||
- **[Custom commands](./cli/custom-commands.md):** Create shortcuts for
|
||||
frequently used prompts.
|
||||
- **[System prompt override](./cli/system-prompt.md):** Customize the core
|
||||
instructions given to the model.
|
||||
- **[Telemetry](./cli/telemetry.md):** Understand how usage data is collected
|
||||
and managed.
|
||||
- **[Enterprise](./cli/enterprise.md):** Manage configurations and policies for
|
||||
large teams.
|
||||
|
||||
- **[Introduction: Extensions](./extensions/index.md):** How to extend the CLI
|
||||
with new functionality.
|
||||
## Extensions
|
||||
|
||||
Extend Gemini CLI's capabilities with new tools and behaviors using extensions.
|
||||
|
||||
- **[Introduction](./extensions/index.md):** Learn about the extension system
|
||||
and how to manage extensions.
|
||||
- **[Writing extensions](./extensions/writing-extensions.md):** Learn how to
|
||||
build your own extension.
|
||||
- **[Extension releasing](./extensions/releasing.md):** How to release Gemini
|
||||
CLI extensions.
|
||||
create your first extension.
|
||||
- **[Extensions reference](./extensions/reference.md):** Deeply understand the
|
||||
extension format, commands, and configuration.
|
||||
- **[Best practices](./extensions/best-practices.md):** Learn strategies for
|
||||
building great extensions.
|
||||
- **[Extensions releasing](./extensions/releasing.md):** Learn how to share your
|
||||
extensions with the world.
|
||||
|
||||
### Hooks
|
||||
## Ecosystem and extensibility
|
||||
|
||||
- **[Hooks](./hooks/index.md):** Intercept and customize Gemini CLI behavior at
|
||||
key lifecycle points.
|
||||
- **[Writing Hooks](./hooks/writing-hooks.md):** Learn how to create your first
|
||||
hook with a comprehensive example.
|
||||
- **[Best Practices](./hooks/best-practices.md):** Security, performance, and
|
||||
debugging guidelines for hooks.
|
||||
Connect Gemini CLI to external services and other development tools.
|
||||
|
||||
### IDE integration
|
||||
- **[MCP servers](./tools/mcp-server.md):** Connect to external services using
|
||||
the Model Context Protocol.
|
||||
- **[IDE integration](./ide-integration/index.md):** Use Gemini CLI alongside VS
|
||||
Code.
|
||||
- **[Hooks](./hooks/index.md):** (Preview) Write scripts that run on specific
|
||||
CLI events.
|
||||
- **[Agent skills](./cli/skills.md):** (Preview) Add specialized expertise and
|
||||
workflows.
|
||||
- **[Sub-agents](./core/subagents.md):** (Preview) Delegate tasks to specialized
|
||||
agents.
|
||||
|
||||
- **[Introduction to IDE integration](./ide-integration/index.md):** Connect the
|
||||
CLI to your editor.
|
||||
- **[IDE companion extension spec](./ide-integration/ide-companion-spec.md):**
|
||||
Spec for building IDE companion extensions.
|
||||
## Development and reference
|
||||
|
||||
### Development
|
||||
Deep dive into the architecture and contribute to the project.
|
||||
|
||||
- **[NPM](./npm.md):** Details on how the project's packages are structured.
|
||||
- **[Releases](./releases.md):** Information on the project's releases and
|
||||
deployment cadence.
|
||||
- **[Changelog](./changelogs/index.md):** Highlights and notable changes to
|
||||
- **[Architecture](./architecture.md):** Understand the technical design of
|
||||
Gemini CLI.
|
||||
- **[Integration tests](./integration-tests.md):** Information about the
|
||||
integration testing framework used in this project.
|
||||
- **[Issue and PR automation](./issue-and-pr-automation.md):** A detailed
|
||||
overview of the automated processes we use to manage and triage issues and
|
||||
pull requests.
|
||||
|
||||
### Support
|
||||
|
||||
- **[FAQ](./faq.md):** Frequently asked questions.
|
||||
- **[Troubleshooting guide](./troubleshooting.md):** Find solutions to common
|
||||
problems.
|
||||
- **[Quota and pricing](./quota-and-pricing.md):** Learn about the free tier and
|
||||
paid options.
|
||||
- **[Terms of service and privacy notice](./tos-privacy.md):** Information on
|
||||
the terms of service and privacy notices applicable to your use of Gemini CLI.
|
||||
|
||||
We hope this documentation helps you make the most of Gemini CLI!
|
||||
- **[Command reference](./cli/commands.md):** A complete list of available
|
||||
commands.
|
||||
- **[Local development](./local-development.md):** Set up your environment to
|
||||
contribute to Gemini CLI.
|
||||
- **[Contributing](../CONTRIBUTING.md):** Learn how to submit pull requests and
|
||||
report issues.
|
||||
- **[FAQ](./faq.md):** Answers to common questions.
|
||||
- **[Troubleshooting](./troubleshooting.md):** Solutions for common issues.
|
||||
|
||||
+14
-2
@@ -7,7 +7,8 @@
|
||||
{ "label": "Installation", "slug": "docs/get-started/installation" },
|
||||
{ "label": "Authentication", "slug": "docs/get-started/authentication" },
|
||||
{ "label": "Examples", "slug": "docs/get-started/examples" },
|
||||
{ "label": "Gemini 3 (preview)", "slug": "docs/get-started/gemini-3" }
|
||||
{ "label": "Gemini 3 (preview)", "slug": "docs/get-started/gemini-3" },
|
||||
{ "label": "CLI Reference", "slug": "docs/cli/cli-reference" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -78,6 +79,10 @@
|
||||
"label": "Ecosystem and extensibility",
|
||||
"items": [
|
||||
{ "label": "Agent skills", "slug": "docs/cli/skills" },
|
||||
{
|
||||
"label": "Creating Agent skills",
|
||||
"slug": "docs/cli/creating-skills"
|
||||
},
|
||||
{
|
||||
"label": "Sub-agents (experimental)",
|
||||
"slug": "docs/core/subagents"
|
||||
@@ -118,7 +123,14 @@
|
||||
"items": [
|
||||
{ "label": "FAQ", "slug": "docs/faq" },
|
||||
{ "label": "Quota and pricing", "slug": "docs/quota-and-pricing" },
|
||||
{ "label": "Release notes", "slug": "docs/changelogs/" },
|
||||
{
|
||||
"label": "Releases",
|
||||
"items": [
|
||||
{ "label": "Release notes", "slug": "docs/changelogs/" },
|
||||
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
|
||||
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
|
||||
]
|
||||
},
|
||||
{ "label": "Terms and privacy", "slug": "docs/tos-privacy" },
|
||||
{ "label": "Troubleshooting", "slug": "docs/troubleshooting" },
|
||||
{ "label": "Uninstall", "slug": "docs/cli/uninstall" }
|
||||
|
||||
@@ -739,10 +739,21 @@ The MCP integration tracks several states:
|
||||
cautiously and only for servers you completely control
|
||||
- **Access tokens:** Be security-aware when configuring environment variables
|
||||
containing API keys or tokens
|
||||
- **Environment variable redaction:** By default, the Gemini CLI redacts
|
||||
sensitive environment variables (such as `GEMINI_API_KEY`, `GOOGLE_API_KEY`,
|
||||
and variables matching patterns like `*TOKEN*`, `*SECRET*`, `*PASSWORD*`) when
|
||||
spawning MCP servers using the `stdio` transport. This prevents unintended
|
||||
exposure of your credentials to third-party servers.
|
||||
- **Explicit environment variables:** If you need to pass a specific environment
|
||||
variable to an MCP server, you should define it explicitly in the `env`
|
||||
property of the server configuration in `settings.json`.
|
||||
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are
|
||||
available within the sandbox environment
|
||||
available within the sandbox environment.
|
||||
- **Private data:** Using broadly scoped personal access tokens can lead to
|
||||
information leakage between repositories
|
||||
information leakage between repositories.
|
||||
- **Untrusted servers:** Be extremely cautious when adding MCP servers from
|
||||
untrusted or third-party sources. Malicious servers could attempt to
|
||||
exfiltrate data or perform unauthorized actions through the tools they expose.
|
||||
|
||||
### Performance and resource management
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ describe('generalist_agent', () => {
|
||||
prompt:
|
||||
'Please use the generalist agent to create a file called "generalist_test_file.txt" containing exactly the following text: success',
|
||||
assert: async (rig) => {
|
||||
// 1) Verify the generalist agent was invoked via delegate_to_agent
|
||||
// 1) Verify the generalist agent was invoked
|
||||
const foundToolCall = await rig.waitForToolCall('generalist');
|
||||
expect(
|
||||
foundToolCall,
|
||||
'Expected to find a delegate_to_agent tool call for generalist agent',
|
||||
'Expected to find a tool call for generalist agent',
|
||||
).toBeTruthy();
|
||||
|
||||
// 2) Verify the file was created as expected
|
||||
|
||||
+23
-10
@@ -34,6 +34,10 @@ export type EvalPolicy = 'ALWAYS_PASSES' | 'USUALLY_PASSES';
|
||||
export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
const fn = async () => {
|
||||
const rig = new TestRig();
|
||||
const { logDir, sanitizedName } = await prepareLogDir(evalCase.name);
|
||||
const activityLogFile = path.join(logDir, `${sanitizedName}.jsonl`);
|
||||
const logFile = path.join(logDir, `${sanitizedName}.log`);
|
||||
let isSuccess = false;
|
||||
try {
|
||||
rig.setup(evalCase.name, evalCase.params);
|
||||
|
||||
@@ -62,6 +66,9 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
const result = await rig.run({
|
||||
args: evalCase.prompt,
|
||||
approvalMode: evalCase.approvalMode ?? 'yolo',
|
||||
env: {
|
||||
GEMINI_CLI_ACTIVITY_LOG_FILE: activityLogFile,
|
||||
},
|
||||
});
|
||||
|
||||
const unauthorizedErrorPrefix =
|
||||
@@ -73,9 +80,16 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
}
|
||||
|
||||
await evalCase.assert(rig, result);
|
||||
isSuccess = true;
|
||||
} finally {
|
||||
await logToFile(
|
||||
evalCase.name,
|
||||
if (isSuccess) {
|
||||
await fs.promises.unlink(activityLogFile).catch((err) => {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
});
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(
|
||||
logFile,
|
||||
JSON.stringify(rig.readToolLogs(), null, 2),
|
||||
);
|
||||
await rig.cleanup();
|
||||
@@ -89,6 +103,13 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareLogDir(name: string) {
|
||||
const logDir = path.resolve(process.cwd(), 'evals/logs');
|
||||
await fs.promises.mkdir(logDir, { recursive: true });
|
||||
const sanitizedName = name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
||||
return { logDir, sanitizedName };
|
||||
}
|
||||
|
||||
export interface EvalCase {
|
||||
name: string;
|
||||
params?: Record<string, any>;
|
||||
@@ -97,11 +118,3 @@ export interface EvalCase {
|
||||
approvalMode?: 'default' | 'auto_edit' | 'yolo' | 'plan';
|
||||
assert: (rig: TestRig, result: string) => Promise<void>;
|
||||
}
|
||||
|
||||
async function logToFile(name: string, content: string) {
|
||||
const logDir = 'evals/logs';
|
||||
await fs.promises.mkdir(logDir, { recursive: true });
|
||||
const sanitizedName = name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
||||
const logFile = `${logDir}/${sanitizedName}.log`;
|
||||
await fs.promises.writeFile(logFile, content);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { GitService, Storage } from '@google/gemini-cli-core';
|
||||
|
||||
describe('Checkpointing Integration', () => {
|
||||
let tmpDir: string;
|
||||
let projectRoot: string;
|
||||
let fakeHome: string;
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'gemini-checkpoint-test-'),
|
||||
);
|
||||
projectRoot = path.join(tmpDir, 'project');
|
||||
fakeHome = path.join(tmpDir, 'home');
|
||||
|
||||
await fs.mkdir(projectRoot, { recursive: true });
|
||||
await fs.mkdir(fakeHome, { recursive: true });
|
||||
|
||||
// Save original env
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
// Simulate environment with NO global gitconfig
|
||||
process.env['HOME'] = fakeHome;
|
||||
delete process.env['GIT_CONFIG_GLOBAL'];
|
||||
delete process.env['GIT_CONFIG_SYSTEM'];
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Restore env
|
||||
process.env = originalEnv;
|
||||
|
||||
// Cleanup
|
||||
try {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
console.error('Failed to cleanup temp dir', e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should successfully create and restore snapshots without global git config', async () => {
|
||||
const storage = new Storage(projectRoot);
|
||||
const gitService = new GitService(projectRoot, storage);
|
||||
|
||||
// 1. Initialize
|
||||
await gitService.initialize();
|
||||
|
||||
// Verify system config empty file creation
|
||||
// We need to access getHistoryDir logic or replicate it.
|
||||
// Since we don't have access to private getHistoryDir, we can infer it or just trust the functional test.
|
||||
|
||||
// 2. Create initial state
|
||||
await fs.writeFile(path.join(projectRoot, 'file1.txt'), 'version 1');
|
||||
await fs.writeFile(path.join(projectRoot, 'file2.txt'), 'permanent file');
|
||||
|
||||
// 3. Create Snapshot
|
||||
const snapshotHash = await gitService.createFileSnapshot('Checkpoint 1');
|
||||
expect(snapshotHash).toBeDefined();
|
||||
|
||||
// 4. Modify files
|
||||
await fs.writeFile(
|
||||
path.join(projectRoot, 'file1.txt'),
|
||||
'version 2 (BAD CHANGE)',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(projectRoot, 'file3.txt'),
|
||||
'new file (SHOULD BE GONE)',
|
||||
);
|
||||
await fs.rm(path.join(projectRoot, 'file2.txt'));
|
||||
|
||||
// 5. Restore
|
||||
await gitService.restoreProjectFromSnapshot(snapshotHash);
|
||||
|
||||
// 6. Verify state
|
||||
const file1Content = await fs.readFile(
|
||||
path.join(projectRoot, 'file1.txt'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(file1Content).toBe('version 1');
|
||||
|
||||
const file2Exists = await fs
|
||||
.stat(path.join(projectRoot, 'file2.txt'))
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(file2Exists).toBe(true);
|
||||
const file2Content = await fs.readFile(
|
||||
path.join(projectRoot, 'file2.txt'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(file2Content).toBe('permanent file');
|
||||
|
||||
const file3Exists = await fs
|
||||
.stat(path.join(projectRoot, 'file3.txt'))
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(file3Exists).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore user global git config and use isolated identity', async () => {
|
||||
// 1. Create a fake global gitconfig with a specific user
|
||||
const globalConfigPath = path.join(fakeHome, '.gitconfig');
|
||||
const globalConfigContent = `[user]
|
||||
name = Global User
|
||||
email = global@example.com
|
||||
`;
|
||||
await fs.writeFile(globalConfigPath, globalConfigContent);
|
||||
|
||||
// Point HOME to fakeHome so git picks up this global config (if we didn't isolate it)
|
||||
process.env['HOME'] = fakeHome;
|
||||
// Ensure GIT_CONFIG_GLOBAL is NOT set for the process initially,
|
||||
// so it would default to HOME/.gitconfig if GitService didn't override it.
|
||||
delete process.env['GIT_CONFIG_GLOBAL'];
|
||||
|
||||
const storage = new Storage(projectRoot);
|
||||
const gitService = new GitService(projectRoot, storage);
|
||||
|
||||
await gitService.initialize();
|
||||
|
||||
// 2. Create a file and snapshot
|
||||
await fs.writeFile(path.join(projectRoot, 'test.txt'), 'content');
|
||||
await gitService.createFileSnapshot('Snapshot with global config present');
|
||||
|
||||
// 3. Verify the commit author in the shadow repo
|
||||
const historyDir = storage.getHistoryDir();
|
||||
|
||||
const { execFileSync } = await import('node:child_process');
|
||||
|
||||
const logOutput = execFileSync(
|
||||
'git',
|
||||
['log', '-1', '--pretty=format:%an <%ae>'],
|
||||
{
|
||||
cwd: historyDir,
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_DIR: path.join(historyDir, '.git'),
|
||||
GIT_CONFIG_GLOBAL: path.join(historyDir, '.gitconfig'),
|
||||
GIT_CONFIG_SYSTEM: path.join(historyDir, '.gitconfig_system_empty'),
|
||||
},
|
||||
encoding: 'utf-8',
|
||||
},
|
||||
);
|
||||
|
||||
expect(logOutput).toBe('Gemini CLI <gemini-cli@google.com>');
|
||||
expect(logOutput).not.toContain('Global User');
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import { mkdir, readdir, rm } from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { canUseRipgrep } from '../packages/core/src/tools/ripGrep.js';
|
||||
import { disableMouseTracking } from '@google/gemini-cli-core';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
@@ -72,6 +73,11 @@ export async function setup() {
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
// Disable mouse tracking
|
||||
if (process.stdout.isTTY) {
|
||||
disableMouseTracking();
|
||||
}
|
||||
|
||||
// Cleanup the test run directory unless KEEP_OUTPUT is set
|
||||
if (process.env['KEEP_OUTPUT'] !== 'true' && runDir) {
|
||||
try {
|
||||
|
||||
@@ -484,9 +484,19 @@ console.log(JSON.stringify({
|
||||
'hooks-system.before-tool-selection.responses',
|
||||
),
|
||||
});
|
||||
// Create inline hook command (works on both Unix and Windows)
|
||||
const hookCommand =
|
||||
"node -e \"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'BeforeToolSelection', toolConfig: {mode: 'ANY', allowedFunctionNames: ['read_file', 'run_shell_command']}}}))\"";
|
||||
|
||||
// Write hook script to file (inline node -e has quoting issues on Windows)
|
||||
const hookScript = `console.log(JSON.stringify({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'BeforeToolSelection',
|
||||
toolConfig: {
|
||||
mode: 'ANY',
|
||||
allowedFunctionNames: ['read_file', 'run_shell_command']
|
||||
}
|
||||
}
|
||||
}));`;
|
||||
const scriptPath = join(rig.testDir!, 'before_tool_selection_hook.cjs');
|
||||
writeFileSync(scriptPath, hookScript);
|
||||
|
||||
rig.setup('should modify tool selection with BeforeToolSelection hooks', {
|
||||
settings: {
|
||||
@@ -500,7 +510,7 @@ console.log(JSON.stringify({
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: hookCommand,
|
||||
command: `node "${scriptPath.replace(/\\/g, '/')}"`,
|
||||
timeout: 5000,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -564,11 +564,17 @@ describe('run_shell_command', () => {
|
||||
|
||||
it('rejects invalid shell expressions', async () => {
|
||||
await rig.setup('rejects invalid shell expressions', {
|
||||
settings: { tools: { core: ['run_shell_command'] } },
|
||||
settings: {
|
||||
tools: {
|
||||
core: ['run_shell_command'],
|
||||
allowed: ['run_shell_command(echo)'], // Specifically allow echo
|
||||
},
|
||||
},
|
||||
});
|
||||
const invalidCommand = getInvalidCommand();
|
||||
const result = await rig.run({
|
||||
args: `I am testing the error handling of the run_shell_command tool. Please attempt to run the following command, which I know has invalid syntax: \`${invalidCommand}\`. If the command fails as expected, please return the word FAIL, otherwise return the word SUCCESS.`,
|
||||
approvalMode: 'default', // Use default mode so safety fallback triggers confirmation
|
||||
});
|
||||
expect(result).toContain('FAIL');
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I could not find the file `nonexistent-file-that-does-not-exist.txt` in the current directory or its subdirectories. Please verify the file path or name."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":25,"totalTokenCount":35,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10}]}}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Hello! How can I help you today?"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":5,"candidatesTokenCount":9,"totalTokenCount":14,"promptTokensDetails":[{"modality":"TEXT","tokenCount":5}]}}]}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { join } from 'node:path';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('stdout-stderr-output', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it('should send model response to stdout and app messages to stderr', async ({
|
||||
signal,
|
||||
}) => {
|
||||
await rig.setup('prompt-output-test', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'stdout-stderr-output.responses',
|
||||
),
|
||||
});
|
||||
|
||||
const { stdout, exitCode } = await rig.runWithStreams(['-p', 'Say hello'], {
|
||||
signal,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.toLowerCase()).toContain('hello');
|
||||
expect(stdout).not.toMatch(/^\[ERROR\]/m);
|
||||
expect(stdout).not.toMatch(/^\[INFO\]/m);
|
||||
});
|
||||
|
||||
it('should handle missing file with message to stdout and error to stderr', async ({
|
||||
signal,
|
||||
}) => {
|
||||
await rig.setup('error-output-test', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'stdout-stderr-output-error.responses',
|
||||
),
|
||||
});
|
||||
|
||||
const { stdout, exitCode } = await rig.runWithStreams(
|
||||
['-p', '@nonexistent-file-that-does-not-exist.txt explain this'],
|
||||
{ signal },
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.toLowerCase()).toMatch(
|
||||
/could not find|not exist|does not exist/,
|
||||
);
|
||||
});
|
||||
});
|
||||
Generated
+8
-31
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -2251,7 +2251,6 @@
|
||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.2",
|
||||
@@ -2432,7 +2431,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -2466,7 +2464,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
||||
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
@@ -2835,7 +2832,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
||||
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -2869,7 +2865,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
||||
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1"
|
||||
@@ -2922,7 +2917,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
||||
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1",
|
||||
@@ -4128,7 +4122,6 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4406,7 +4399,6 @@
|
||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.35.0",
|
||||
"@typescript-eslint/types": "8.35.0",
|
||||
@@ -5399,7 +5391,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8409,7 +8400,6 @@
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8950,7 +8940,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -10552,7 +10541,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
||||
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -14311,7 +14299,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14322,7 +14309,6 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -16559,7 +16545,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16783,8 +16768,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -16792,7 +16776,6 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -16965,7 +16948,6 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -17173,7 +17155,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -17287,7 +17268,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -17300,7 +17280,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -18005,7 +17984,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -18021,7 +17999,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.8",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -18077,7 +18055,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
@@ -18164,7 +18142,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.8",
|
||||
@@ -18300,7 +18278,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -18323,7 +18300,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -18340,7 +18317,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-nightly.20260121.97aac696f"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.28.0-nightly.20260128.adc8e11bb"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-nightly.20260121.97aac696f"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.28.0-nightly.20260128.adc8e11bb"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type ExtensionSetting,
|
||||
} from '../../config/extensions/extensionSettings.js';
|
||||
import prompts from 'prompts';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
const {
|
||||
mockExtensionManager,
|
||||
@@ -79,11 +80,15 @@ vi.mock('../../config/settings.js', () => ({
|
||||
}));
|
||||
|
||||
describe('extensions configure command', () => {
|
||||
let tempWorkspaceDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(debugLogger, 'log');
|
||||
vi.spyOn(debugLogger, 'error');
|
||||
vi.clearAllMocks();
|
||||
|
||||
tempWorkspaceDir = fs.mkdtempSync('gemini-cli-test-workspace');
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
||||
// Default behaviors
|
||||
mockLoadSettings.mockReturnValue({ merged: {} });
|
||||
mockGetExtensionAndManager.mockResolvedValue({
|
||||
@@ -141,6 +146,7 @@ describe('extensions configure command', () => {
|
||||
'TEST_VAR',
|
||||
promptForSetting,
|
||||
'user',
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -186,6 +192,7 @@ describe('extensions configure command', () => {
|
||||
'VAR_1',
|
||||
promptForSetting,
|
||||
'user',
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ async function configureSpecificSetting(
|
||||
settingKey,
|
||||
promptForSetting,
|
||||
scope,
|
||||
process.cwd(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,6 +219,7 @@ async function configureExtensionSettings(
|
||||
setting.envVar,
|
||||
promptForSetting,
|
||||
scope,
|
||||
process.cwd(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Themes Example
|
||||
|
||||
This is an example of a Gemini CLI extension that adds a custom theme.
|
||||
|
||||
## How to use
|
||||
|
||||
1. Link this extension:
|
||||
|
||||
```bash
|
||||
gemini extensions link packages/cli/src/commands/extensions/examples/themes-example
|
||||
```
|
||||
|
||||
2. Set the theme in your settings file (`~/.gemini/config.yaml`):
|
||||
|
||||
```yaml
|
||||
ui:
|
||||
theme: 'shades-of-green-theme (themes-example)'
|
||||
```
|
||||
|
||||
Alternatively, you can set it through the UI by running `gemini` and then
|
||||
typing `/theme` and pressing Enter.
|
||||
|
||||
3. **Observe the Changes:**
|
||||
|
||||
After setting the theme, you should see the changes reflected in the Gemini
|
||||
CLI's UI. The background will be a dark green, the primary text a lighter
|
||||
green, and various other UI elements will display different shades of green,
|
||||
as defined in this extension's `gemini-extension.json` file.
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "themes-example",
|
||||
"version": "1.0.0",
|
||||
"themes": [
|
||||
{
|
||||
"name": "shades-of-green-theme",
|
||||
"type": "custom",
|
||||
"background": {
|
||||
"primary": "#1a362a"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#a6e3a1",
|
||||
"secondary": "#6e8e7a",
|
||||
"link": "#89e689"
|
||||
},
|
||||
"status": {
|
||||
"success": "#76c076",
|
||||
"warning": "#d9e689",
|
||||
"error": "#b34e4e"
|
||||
},
|
||||
"border": {
|
||||
"default": "#4a6c5a"
|
||||
},
|
||||
"ui": {
|
||||
"comment": "#6e8e7a"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { ExtensionUpdateState } from '../../ui/state/extensions.js';
|
||||
|
||||
// Mock dependencies
|
||||
const emitConsoleLog = vi.hoisted(() => vi.fn());
|
||||
const emitFeedback = vi.hoisted(() => vi.fn());
|
||||
const debugLogger = vi.hoisted(() => ({
|
||||
log: vi.fn((message, ...args) => {
|
||||
emitConsoleLog('log', format(message, ...args));
|
||||
@@ -40,6 +41,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
...actual,
|
||||
coreEvents: {
|
||||
emitConsoleLog,
|
||||
emitFeedback,
|
||||
},
|
||||
debugLogger,
|
||||
};
|
||||
@@ -84,6 +86,42 @@ describe('extensions update command', () => {
|
||||
});
|
||||
|
||||
describe('handleUpdate', () => {
|
||||
it('should list installed extensions when requested extension is not found', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
const extensions = [
|
||||
{ name: 'ext1', version: '1.0.0' },
|
||||
{ name: 'ext2', version: '2.0.0' },
|
||||
];
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue(extensions);
|
||||
|
||||
await handleUpdate({ name: 'missing-extension' });
|
||||
|
||||
expect(emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Extension "missing-extension" not found.\n\nInstalled extensions:\next1 (1.0.0)\next2 (2.0.0)\n\nRun "gemini extensions list" for details.',
|
||||
);
|
||||
expect(mockUpdateExtension).not.toHaveBeenCalled();
|
||||
mockCwd.mockRestore();
|
||||
});
|
||||
|
||||
it('should log a helpful message when no extensions are installed and requested extension is not found', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue([]);
|
||||
|
||||
await handleUpdate({ name: 'missing-extension' });
|
||||
|
||||
expect(emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Extension "missing-extension" not found.\n\nNo extensions installed.',
|
||||
);
|
||||
expect(mockUpdateExtension).not.toHaveBeenCalled();
|
||||
mockCwd.mockRestore();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
state: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { checkForExtensionUpdate } from '../../config/extensions/github.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { coreEvents, debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
@@ -46,7 +46,21 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
(extension) => extension.name === args.name,
|
||||
);
|
||||
if (!extension) {
|
||||
debugLogger.log(`Extension "${args.name}" not found.`);
|
||||
if (extensions.length === 0) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Extension "${args.name}" not found.\n\nNo extensions installed.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const installedExtensions = extensions
|
||||
.map((extension) => `${extension.name} (${extension.version})`)
|
||||
.join('\n');
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Extension "${args.name}" not found.\n\nInstalled extensions:\n${installedExtensions}\n\nRun "gemini extensions list" for details.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!extension.installMetadata) {
|
||||
@@ -63,7 +77,6 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
debugLogger.log(`Extension "${args.name}" is already up to date.`);
|
||||
return;
|
||||
}
|
||||
// TODO(chrstnb): we should list extensions if the requested extension is not installed.
|
||||
const updatedExtensionInfo = (await updateExtension(
|
||||
extension,
|
||||
extensionManager,
|
||||
|
||||
@@ -128,6 +128,13 @@ async function addMcpServer(
|
||||
|
||||
settings.setValue(settingsScope, 'mcpServers', mcpServers);
|
||||
|
||||
if (transport === 'stdio') {
|
||||
debugLogger.warn(
|
||||
'Security Warning: Running MCP servers with stdio transport can expose inherited environment variables. ' +
|
||||
'While the Gemini CLI redacts common API keys and secrets by default, you should only run servers from trusted sources.',
|
||||
);
|
||||
}
|
||||
|
||||
if (isExistingServer) {
|
||||
debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`);
|
||||
} else {
|
||||
|
||||
@@ -588,7 +588,7 @@ describe('parseArguments', () => {
|
||||
process.argv = ['node', 'script.js', 'skills', 'list'];
|
||||
// Skills command enabled by default or via experimental
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: { skills: true },
|
||||
skills: { enabled: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.isCommand).toBe(true);
|
||||
@@ -596,9 +596,9 @@ describe('parseArguments', () => {
|
||||
|
||||
it('should set isCommand to true for hooks command', async () => {
|
||||
process.argv = ['node', 'script.js', 'hooks', 'migrate'];
|
||||
// Hooks command enabled via tools settings
|
||||
// Hooks command enabled via hooksConfig settings
|
||||
const settings = createTestMergedSettings({
|
||||
tools: { enableHooks: true },
|
||||
hooksConfig: { enabled: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.isCommand).toBe(true);
|
||||
@@ -1255,7 +1255,7 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
});
|
||||
|
||||
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
|
||||
'Cannot start in YOLO mode since it is disabled by your admin',
|
||||
'YOLO mode is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2928,7 +2928,7 @@ describe('loadCliConfig disableYoloMode', () => {
|
||||
security: { disableYoloMode: true },
|
||||
});
|
||||
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
|
||||
'Cannot start in YOLO mode since it is disabled by your admin',
|
||||
'YOLO mode is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2960,7 +2960,7 @@ describe('loadCliConfig secureModeEnabled', () => {
|
||||
});
|
||||
|
||||
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
|
||||
'Cannot start in YOLO mode since it is disabled by your admin',
|
||||
'YOLO mode is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2974,7 +2974,7 @@ describe('loadCliConfig secureModeEnabled', () => {
|
||||
});
|
||||
|
||||
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
|
||||
'Cannot start in YOLO mode since it is disabled by your admin',
|
||||
'YOLO mode is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
type OutputFormat,
|
||||
coreEvents,
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
getAdminErrorMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
type Settings,
|
||||
@@ -304,11 +305,11 @@ export async function parseArguments(
|
||||
yargsInstance.command(extensionsCommand);
|
||||
}
|
||||
|
||||
if (settings.experimental?.skills || (settings.skills?.enabled ?? true)) {
|
||||
if (settings.skills?.enabled ?? true) {
|
||||
yargsInstance.command(skillsCommand);
|
||||
}
|
||||
// Register hooks command if hooks are enabled
|
||||
if (settings.tools?.enableHooks) {
|
||||
if (settings.hooksConfig.enabled) {
|
||||
yargsInstance.command(hooksCommand);
|
||||
}
|
||||
|
||||
@@ -550,7 +551,7 @@ export async function loadCliConfig(
|
||||
);
|
||||
}
|
||||
throw new FatalConfigError(
|
||||
'Cannot start in YOLO mode since it is disabled by your admin',
|
||||
getAdminErrorMessage('YOLO mode', undefined /* config */),
|
||||
);
|
||||
}
|
||||
} else if (approvalMode === ApprovalMode.YOLO) {
|
||||
@@ -755,13 +756,13 @@ export async function loadCliConfig(
|
||||
plan: settings.experimental?.plan,
|
||||
enableEventDrivenScheduler:
|
||||
settings.experimental?.enableEventDrivenScheduler,
|
||||
skillsSupport:
|
||||
settings.experimental?.skills || (settings.skills?.enabled ?? true),
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
ideMode,
|
||||
disableLoopDetection: settings.model?.disableLoopDetection,
|
||||
compressionThreshold: settings.model?.compressionThreshold,
|
||||
folderTrust,
|
||||
interactive,
|
||||
@@ -791,10 +792,8 @@ export async function loadCliConfig(
|
||||
acceptRawOutputRisk: argv.acceptRawOutputRisk,
|
||||
modelConfigServiceConfig: settings.modelConfigs,
|
||||
// TODO: loading of hooks based on workspace trust
|
||||
enableHooks:
|
||||
(settings.tools?.enableHooks ?? true) &&
|
||||
(settings.hooksConfig?.enabled ?? true),
|
||||
enableHooksUI: settings.tools?.enableHooks ?? true,
|
||||
enableHooks: settings.hooksConfig.enabled,
|
||||
enableHooksUI: settings.hooksConfig.enabled,
|
||||
hooks: settings.hooks || {},
|
||||
disabledHooks: settings.hooksConfig?.disabled || [],
|
||||
projectHooks: projectHooks || {},
|
||||
|
||||
@@ -227,7 +227,6 @@ System using model: \${MODEL_NAME}
|
||||
settings: createTestMergedSettings({
|
||||
telemetry: { enabled: false },
|
||||
experimental: { extensionConfig: true },
|
||||
tools: { enableHooks: true },
|
||||
hooksConfig: { enabled: true },
|
||||
}),
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
@@ -306,6 +305,7 @@ System using model: \${MODEL_NAME}
|
||||
'MY_VALUE',
|
||||
mockRequestSetting,
|
||||
ExtensionSettingScope.USER,
|
||||
process.cwd(),
|
||||
);
|
||||
|
||||
await extensionManager.restartExtension(extension);
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
beforeAll,
|
||||
afterAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
afterEach,
|
||||
} from 'vitest';
|
||||
import { createExtension } from '../test-utils/createExtension.js';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
|
||||
import { GEMINI_DIR, type Config } from '@google/gemini-cli-core';
|
||||
import { createTestMergedSettings, SettingScope } from './settings.js';
|
||||
|
||||
describe('ExtensionManager theme loading', () => {
|
||||
let extensionManager: ExtensionManager;
|
||||
let userExtensionsDir: string;
|
||||
let tempHomeDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tempHomeDir = await fs.promises.mkdtemp(
|
||||
path.join(fs.realpathSync('/tmp'), 'gemini-cli-test-'),
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (tempHomeDir) {
|
||||
await fs.promises.rm(tempHomeDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
process.env['GEMINI_CLI_HOME'] = tempHomeDir;
|
||||
userExtensionsDir = path.join(tempHomeDir, GEMINI_DIR, 'extensions');
|
||||
// Ensure userExtensionsDir is clean for each test
|
||||
fs.rmSync(userExtensionsDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
settings: createTestMergedSettings({
|
||||
experimental: { extensionConfig: true },
|
||||
security: { blockGitExtensions: false },
|
||||
admin: { extensions: { enabled: true }, mcp: { enabled: true } },
|
||||
}),
|
||||
requestConsent: async () => true,
|
||||
requestSetting: async () => '',
|
||||
workspaceDir: tempHomeDir,
|
||||
enabledExtensionOverrides: [],
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
themeManager.clearExtensionThemes();
|
||||
themeManager.loadCustomThemes({});
|
||||
themeManager.setActiveTheme(DEFAULT_THEME.name);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env['GEMINI_CLI_HOME'];
|
||||
});
|
||||
|
||||
it('should register themes from an extension when started', async () => {
|
||||
const registerSpy = vi.spyOn(themeManager, 'registerExtensionThemes');
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'my-theme-extension',
|
||||
themes: [
|
||||
{
|
||||
name: 'My-Awesome-Theme',
|
||||
type: 'custom',
|
||||
text: {
|
||||
primary: '#FF00FF',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
const mockConfig = {
|
||||
getEnableExtensionReloading: () => false,
|
||||
getMcpClientManager: () => ({
|
||||
startExtension: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
getGeminiClient: () => ({
|
||||
isInitialized: () => false,
|
||||
updateSystemInstruction: vi.fn(),
|
||||
setTools: vi.fn(),
|
||||
}),
|
||||
getHookSystem: () => undefined,
|
||||
getWorkingDir: () => tempHomeDir,
|
||||
shouldLoadMemoryFromIncludeDirectories: () => false,
|
||||
getDebugMode: () => false,
|
||||
getFileExclusions: () => ({
|
||||
isIgnored: () => false,
|
||||
}),
|
||||
getGeminiMdFilePaths: () => [],
|
||||
getMcpServers: () => ({}),
|
||||
getAllowedMcpServers: () => [],
|
||||
getSanitizationConfig: () => ({
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
}),
|
||||
getShellExecutionConfig: () => ({
|
||||
terminalWidth: 80,
|
||||
terminalHeight: 24,
|
||||
showColor: false,
|
||||
pager: 'cat',
|
||||
sanitizationConfig: {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
},
|
||||
}),
|
||||
getToolRegistry: () => ({
|
||||
getTools: () => [],
|
||||
}),
|
||||
getProxy: () => undefined,
|
||||
getFileService: () => ({
|
||||
findFiles: async () => [],
|
||||
}),
|
||||
getExtensionLoader: () => ({
|
||||
getExtensions: () => [],
|
||||
}),
|
||||
isTrustedFolder: () => true,
|
||||
getImportFormat: () => 'tree',
|
||||
} as unknown as Config;
|
||||
|
||||
await extensionManager.start(mockConfig);
|
||||
|
||||
expect(registerSpy).toHaveBeenCalledWith('my-theme-extension', [
|
||||
{
|
||||
name: 'My-Awesome-Theme',
|
||||
type: 'custom',
|
||||
text: {
|
||||
primary: '#FF00FF',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should revert to default theme when extension is stopped', async () => {
|
||||
const extensionName = 'my-theme-extension';
|
||||
const themeName = 'My-Awesome-Theme';
|
||||
const namespacedThemeName = `${themeName} (${extensionName})`;
|
||||
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: extensionName,
|
||||
themes: [
|
||||
{
|
||||
name: themeName,
|
||||
type: 'custom',
|
||||
text: {
|
||||
primary: '#FF00FF',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
const mockConfig = {
|
||||
getWorkingDir: () => tempHomeDir,
|
||||
shouldLoadMemoryFromIncludeDirectories: () => false,
|
||||
getWorkspaceContext: () => ({
|
||||
getDirectories: () => [],
|
||||
}),
|
||||
getDebugMode: () => false,
|
||||
getFileService: () => ({
|
||||
findFiles: async () => [],
|
||||
}),
|
||||
getExtensionLoader: () => ({
|
||||
getExtensions: () => [],
|
||||
}),
|
||||
isTrustedFolder: () => true,
|
||||
getImportFormat: () => 'tree',
|
||||
getFileFilteringOptions: () => ({
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: true,
|
||||
}),
|
||||
getDiscoveryMaxDirs: () => 200,
|
||||
getMcpClientManager: () => ({
|
||||
getMcpInstructions: () => '',
|
||||
startExtension: vi.fn().mockResolvedValue(undefined),
|
||||
stopExtension: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
setUserMemory: vi.fn(),
|
||||
setGeminiMdFileCount: vi.fn(),
|
||||
setGeminiMdFilePaths: vi.fn(),
|
||||
getEnableExtensionReloading: () => true,
|
||||
getGeminiClient: () => ({
|
||||
isInitialized: () => false,
|
||||
updateSystemInstruction: vi.fn(),
|
||||
setTools: vi.fn(),
|
||||
}),
|
||||
getHookSystem: () => undefined,
|
||||
getProxy: () => undefined,
|
||||
getAgentRegistry: () => ({
|
||||
reload: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
} as unknown as Config;
|
||||
|
||||
await extensionManager.start(mockConfig);
|
||||
|
||||
// Set the active theme to the one from the extension
|
||||
themeManager.setActiveTheme(namespacedThemeName);
|
||||
expect(themeManager.getActiveTheme().name).toBe(namespacedThemeName);
|
||||
|
||||
// Stop the extension
|
||||
await extensionManager.disableExtension(extensionName, SettingScope.User);
|
||||
|
||||
// Check that the active theme has reverted to the default
|
||||
expect(themeManager.getActiveTheme().name).toBe(DEFAULT_THEME.name);
|
||||
});
|
||||
});
|
||||
@@ -69,6 +69,7 @@ import {
|
||||
ExtensionSettingScope,
|
||||
} from './extensions/extensionSettings.js';
|
||||
import type { EventEmitter } from 'node:stream';
|
||||
import { themeManager } from '../ui/themes/theme-manager.js';
|
||||
|
||||
interface ExtensionManagerParams {
|
||||
enabledExtensionOverrides?: string[];
|
||||
@@ -143,6 +144,26 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
previousExtensionConfig?: ExtensionConfig,
|
||||
): Promise<GeminiCLIExtension> {
|
||||
if (
|
||||
this.settings.security?.allowedExtensions &&
|
||||
this.settings.security?.allowedExtensions.length > 0
|
||||
) {
|
||||
const extensionAllowed = this.settings.security?.allowedExtensions.some(
|
||||
(pattern) => {
|
||||
try {
|
||||
return new RegExp(pattern).test(installMetadata.source);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
if (!extensionAllowed) {
|
||||
throw new Error(
|
||||
`Installing extension from source "${installMetadata.source}" is not allowed by the "allowedExtensions" security setting.`,
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
(installMetadata.type === 'git' ||
|
||||
installMetadata.type === 'github-release') &&
|
||||
this.settings.security.blockGitExtensions
|
||||
@@ -151,6 +172,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
'Installing extensions from remote sources is disallowed by your current settings.',
|
||||
);
|
||||
}
|
||||
|
||||
const isUpdate = !!previousExtensionConfig;
|
||||
let newExtensionConfig: ExtensionConfig | null = null;
|
||||
let localSourcePath: string | undefined;
|
||||
@@ -468,6 +490,20 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
);
|
||||
}
|
||||
|
||||
protected override async startExtension(extension: GeminiCLIExtension) {
|
||||
await super.startExtension(extension);
|
||||
if (extension.themes) {
|
||||
themeManager.registerExtensionThemes(extension.name, extension.themes);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async stopExtension(extension: GeminiCLIExtension) {
|
||||
await super.stopExtension(extension);
|
||||
if (extension.themes) {
|
||||
themeManager.unregisterExtensionThemes(extension.name, extension.themes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all installed extensions, should only be called once.
|
||||
*/
|
||||
@@ -507,10 +543,39 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
const installMetadata = loadInstallMetadata(extensionDir);
|
||||
let effectiveExtensionPath = extensionDir;
|
||||
if (
|
||||
this.settings.security?.allowedExtensions &&
|
||||
this.settings.security?.allowedExtensions.length > 0
|
||||
) {
|
||||
if (!installMetadata?.source) {
|
||||
throw new Error(
|
||||
`Failed to load extension ${extensionDir}. The ${INSTALL_METADATA_FILENAME} file is missing or misconfigured.`,
|
||||
);
|
||||
}
|
||||
const extensionAllowed = this.settings.security?.allowedExtensions.some(
|
||||
(pattern) => {
|
||||
try {
|
||||
return new RegExp(pattern).test(installMetadata?.source);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
if (!extensionAllowed) {
|
||||
debugLogger.warn(
|
||||
`Failed to load extension ${extensionDir}. This extension is not allowed by the "allowedExtensions" security setting.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} else if (
|
||||
(installMetadata?.type === 'git' ||
|
||||
installMetadata?.type === 'github-release') &&
|
||||
this.settings.security.blockGitExtensions
|
||||
) {
|
||||
debugLogger.warn(
|
||||
`Failed to load extension ${extensionDir}. Extensions from remote sources is disallowed by your current settings.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -624,10 +689,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
};
|
||||
|
||||
let hooks: { [K in HookEventName]?: HookDefinition[] } | undefined;
|
||||
if (
|
||||
this.settings.tools.enableHooks &&
|
||||
this.settings.hooksConfig.enabled
|
||||
) {
|
||||
if (this.settings.hooksConfig.enabled) {
|
||||
hooks = await this.loadExtensionHooks(
|
||||
effectiveExtensionPath,
|
||||
hydrationContext,
|
||||
@@ -698,6 +760,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
resolvedSettings,
|
||||
skills,
|
||||
agents: agentLoadResult.agents,
|
||||
themes: config.themes,
|
||||
};
|
||||
this.loadedExtensions = [...this.loadedExtensions, extension];
|
||||
|
||||
|
||||
@@ -622,6 +622,7 @@ describe('extension tests', () => {
|
||||
});
|
||||
|
||||
it('should not load github extensions if blockGitExtensions is set', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'my-ext',
|
||||
@@ -645,6 +646,73 @@ describe('extension tests', () => {
|
||||
const extension = extensions.find((e) => e.name === 'my-ext');
|
||||
|
||||
expect(extension).toBeUndefined();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Extensions from remote sources is disallowed by your current settings.',
|
||||
),
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should load allowed extensions if the allowlist is set.', async () => {
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'my-ext',
|
||||
version: '1.0.0',
|
||||
installMetadata: {
|
||||
type: 'git',
|
||||
source: 'http://allowed.com/foo/bar',
|
||||
},
|
||||
});
|
||||
const extensionAllowlistSetting = createTestMergedSettings({
|
||||
security: {
|
||||
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
|
||||
},
|
||||
});
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: extensionAllowlistSetting,
|
||||
});
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].name).toBe('my-ext');
|
||||
});
|
||||
|
||||
it('should not load disallowed extensions if the allowlist is set.', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'my-ext',
|
||||
version: '1.0.0',
|
||||
installMetadata: {
|
||||
type: 'git',
|
||||
source: 'http://notallowed.com/foo/bar',
|
||||
},
|
||||
});
|
||||
const extensionAllowlistSetting = createTestMergedSettings({
|
||||
security: {
|
||||
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
|
||||
},
|
||||
});
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: extensionAllowlistSetting,
|
||||
});
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const extension = extensions.find((e) => e.name === 'my-ext');
|
||||
|
||||
expect(extension).toBeUndefined();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'This extension is not allowed by the "allowedExtensions" security setting',
|
||||
),
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not load any extensions if admin.extensions.enabled is false', async () => {
|
||||
@@ -1116,6 +1184,30 @@ describe('extension tests', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not install a disallowed extension if the allowlist is set', async () => {
|
||||
const gitUrl = 'https://somehost.com/somerepo.git';
|
||||
const allowedExtensionsSetting = createTestMergedSettings({
|
||||
security: {
|
||||
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
|
||||
},
|
||||
});
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: allowedExtensionsSetting,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
await expect(
|
||||
extensionManager.installOrUpdateExtension({
|
||||
source: gitUrl,
|
||||
type: 'git',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
`Installing extension from source "${gitUrl}" is not allowed by the "allowedExtensions" security setting.`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should prompt for trust if workspace is not trusted', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: false,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type {
|
||||
MCPServerConfig,
|
||||
ExtensionInstallMetadata,
|
||||
CustomTheme,
|
||||
} from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
@@ -27,6 +28,11 @@ export interface ExtensionConfig {
|
||||
contextFileName?: string | string[];
|
||||
excludeTools?: string[];
|
||||
settings?: ExtensionSetting[];
|
||||
/**
|
||||
* Custom themes contributed by this extension.
|
||||
* These themes will be registered when the extension is activated.
|
||||
*/
|
||||
themes?: CustomTheme[];
|
||||
}
|
||||
|
||||
export interface ExtensionUpdateInfo {
|
||||
|
||||
@@ -398,6 +398,35 @@ describe('extensionSettings', () => {
|
||||
expect(actualContent).toBe('VAR1="a value with spaces"\n');
|
||||
});
|
||||
|
||||
it('should not set sensitive settings if the value is empty during initial setup', async () => {
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
settings: [
|
||||
{
|
||||
name: 's1',
|
||||
description: 'd1',
|
||||
envVar: 'SENSITIVE_VAR',
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
mockRequestSetting.mockResolvedValue('');
|
||||
|
||||
await maybePromptForSettings(
|
||||
config,
|
||||
'12345',
|
||||
mockRequestSetting,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
const userKeychain = new KeychainTokenStorage(
|
||||
`Gemini CLI Extensions test-ext 12345`,
|
||||
);
|
||||
expect(await userKeychain.getSecret('SENSITIVE_VAR')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not attempt to clear secrets if keychain is unavailable', async () => {
|
||||
// Arrange
|
||||
const mockIsAvailable = vi.fn().mockResolvedValue(false);
|
||||
@@ -738,5 +767,42 @@ describe('extensionSettings', () => {
|
||||
const lines = actualContent.split('\n').filter((line) => line.length > 0);
|
||||
expect(lines).toHaveLength(3); // Should only have the three variables
|
||||
});
|
||||
|
||||
it('should delete a sensitive setting if the new value is empty', async () => {
|
||||
mockRequestSetting.mockResolvedValue('');
|
||||
|
||||
await updateSetting(
|
||||
config,
|
||||
'12345',
|
||||
'VAR2',
|
||||
mockRequestSetting,
|
||||
ExtensionSettingScope.USER,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
||||
const userKeychain = new KeychainTokenStorage(
|
||||
`Gemini CLI Extensions test-ext 12345`,
|
||||
);
|
||||
expect(await userKeychain.getSecret('VAR2')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not throw if deleting a non-existent sensitive setting with empty value', async () => {
|
||||
mockRequestSetting.mockResolvedValue('');
|
||||
// Ensure it doesn't exist first
|
||||
const userKeychain = new KeychainTokenStorage(
|
||||
`Gemini CLI Extensions test-ext 12345`,
|
||||
);
|
||||
await userKeychain.deleteSecret('VAR2');
|
||||
|
||||
await updateSetting(
|
||||
config,
|
||||
'12345',
|
||||
'VAR2',
|
||||
mockRequestSetting,
|
||||
ExtensionSettingScope.USER,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
// Should complete without error
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,7 +112,7 @@ export async function maybePromptForSettings(
|
||||
const nonSensitiveSettings: Record<string, string> = {};
|
||||
for (const setting of settings) {
|
||||
const value = allSettings[setting.envVar];
|
||||
if (value === undefined) {
|
||||
if (value === undefined || value === '') {
|
||||
continue;
|
||||
}
|
||||
if (setting.sensitive) {
|
||||
@@ -207,7 +207,7 @@ export async function updateSetting(
|
||||
settingKey: string,
|
||||
requestSetting: (setting: ExtensionSetting) => Promise<string>,
|
||||
scope: ExtensionSettingScope,
|
||||
workspaceDir?: string,
|
||||
workspaceDir: string,
|
||||
): Promise<void> {
|
||||
const { name: extensionName, settings } = extensionConfig;
|
||||
if (!settings || settings.length === 0) {
|
||||
@@ -230,7 +230,15 @@ export async function updateSetting(
|
||||
);
|
||||
|
||||
if (settingToUpdate.sensitive) {
|
||||
await keychain.setSecret(settingToUpdate.envVar, newValue);
|
||||
if (newValue) {
|
||||
await keychain.setSecret(settingToUpdate.envVar, newValue);
|
||||
} else {
|
||||
try {
|
||||
await keychain.deleteSecret(settingToUpdate.envVar);
|
||||
} catch {
|
||||
// Ignore if secret does not exist
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,15 @@ export enum Command {
|
||||
OPEN_EXTERNAL_EDITOR = 'input.openExternalEditor',
|
||||
PASTE_CLIPBOARD = 'input.paste',
|
||||
|
||||
BACKGROUND_SHELL_ESCAPE = 'backgroundShellEscape',
|
||||
BACKGROUND_SHELL_SELECT = 'backgroundShellSelect',
|
||||
TOGGLE_BACKGROUND_SHELL = 'toggleBackgroundShell',
|
||||
TOGGLE_BACKGROUND_SHELL_LIST = 'toggleBackgroundShellList',
|
||||
KILL_BACKGROUND_SHELL = 'backgroundShell.kill',
|
||||
UNFOCUS_BACKGROUND_SHELL = 'backgroundShell.unfocus',
|
||||
UNFOCUS_BACKGROUND_SHELL_LIST = 'backgroundShell.listUnfocus',
|
||||
SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING = 'backgroundShell.unfocusWarning',
|
||||
|
||||
// App Controls
|
||||
SHOW_ERROR_DETAILS = 'app.showErrorDetails',
|
||||
SHOW_FULL_TODOS = 'app.showFullTodos',
|
||||
@@ -85,6 +94,7 @@ export enum Command {
|
||||
UNFOCUS_SHELL_INPUT = 'app.unfocusShellInput',
|
||||
CLEAR_SCREEN = 'app.clearScreen',
|
||||
RESTART_APP = 'app.restart',
|
||||
SUSPEND_APP = 'app.suspend',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,7 +148,6 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
],
|
||||
[Command.MOVE_LEFT]: [
|
||||
{ key: 'left', shift: false, alt: false, ctrl: false, cmd: false },
|
||||
{ key: 'b', ctrl: true },
|
||||
],
|
||||
[Command.MOVE_RIGHT]: [
|
||||
{ key: 'right', shift: false, alt: false, ctrl: false, cmd: false },
|
||||
@@ -170,8 +179,15 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
],
|
||||
[Command.DELETE_CHAR_LEFT]: [{ key: 'backspace' }, { key: 'h', ctrl: true }],
|
||||
[Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
|
||||
[Command.UNDO]: [{ key: 'z', shift: false, ctrl: true }],
|
||||
[Command.REDO]: [{ key: 'z', shift: true, ctrl: true }],
|
||||
[Command.UNDO]: [
|
||||
{ key: 'z', cmd: true, shift: false },
|
||||
{ key: 'z', alt: true, shift: false },
|
||||
],
|
||||
[Command.REDO]: [
|
||||
{ key: 'z', ctrl: true, shift: true },
|
||||
{ key: 'z', cmd: true, shift: true },
|
||||
{ key: 'z', alt: true, shift: true },
|
||||
],
|
||||
|
||||
// Scrolling
|
||||
[Command.SCROLL_UP]: [{ key: 'up', shift: true }],
|
||||
@@ -257,6 +273,16 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
[Command.TOGGLE_COPY_MODE]: [{ key: 's', ctrl: true }],
|
||||
[Command.TOGGLE_YOLO]: [{ key: 'y', ctrl: true }],
|
||||
[Command.CYCLE_APPROVAL_MODE]: [{ key: 'tab', shift: true }],
|
||||
[Command.TOGGLE_BACKGROUND_SHELL]: [{ key: 'b', ctrl: true }],
|
||||
[Command.TOGGLE_BACKGROUND_SHELL_LIST]: [{ key: 'l', ctrl: true }],
|
||||
[Command.KILL_BACKGROUND_SHELL]: [{ key: 'k', ctrl: true }],
|
||||
[Command.UNFOCUS_BACKGROUND_SHELL]: [{ key: 'tab', shift: true }],
|
||||
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]: [{ key: 'tab', shift: false }],
|
||||
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]: [
|
||||
{ key: 'tab', shift: false },
|
||||
],
|
||||
[Command.BACKGROUND_SHELL_SELECT]: [{ key: 'return' }],
|
||||
[Command.BACKGROUND_SHELL_ESCAPE]: [{ key: 'escape' }],
|
||||
[Command.SHOW_MORE_LINES]: [
|
||||
{ key: 'o', ctrl: true },
|
||||
{ key: 's', ctrl: true },
|
||||
@@ -265,6 +291,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
[Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab' }],
|
||||
[Command.CLEAR_SCREEN]: [{ key: 'l', ctrl: true }],
|
||||
[Command.RESTART_APP]: [{ key: 'r' }],
|
||||
[Command.SUSPEND_APP]: [{ key: 'z', ctrl: true }],
|
||||
};
|
||||
|
||||
interface CommandCategory {
|
||||
@@ -370,10 +397,19 @@ export const commandCategories: readonly CommandCategory[] = [
|
||||
Command.TOGGLE_YOLO,
|
||||
Command.CYCLE_APPROVAL_MODE,
|
||||
Command.SHOW_MORE_LINES,
|
||||
Command.TOGGLE_BACKGROUND_SHELL,
|
||||
Command.TOGGLE_BACKGROUND_SHELL_LIST,
|
||||
Command.KILL_BACKGROUND_SHELL,
|
||||
Command.BACKGROUND_SHELL_SELECT,
|
||||
Command.BACKGROUND_SHELL_ESCAPE,
|
||||
Command.UNFOCUS_BACKGROUND_SHELL,
|
||||
Command.UNFOCUS_BACKGROUND_SHELL_LIST,
|
||||
Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING,
|
||||
Command.FOCUS_SHELL_INPUT,
|
||||
Command.UNFOCUS_SHELL_INPUT,
|
||||
Command.CLEAR_SCREEN,
|
||||
Command.RESTART_APP,
|
||||
Command.SUSPEND_APP,
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -460,8 +496,17 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only).',
|
||||
[Command.SHOW_MORE_LINES]:
|
||||
'Expand a height-constrained response to show additional lines when not in alternate buffer mode.',
|
||||
[Command.BACKGROUND_SHELL_SELECT]: 'Enter',
|
||||
[Command.BACKGROUND_SHELL_ESCAPE]: 'Esc',
|
||||
[Command.TOGGLE_BACKGROUND_SHELL]: 'Ctrl+B',
|
||||
[Command.TOGGLE_BACKGROUND_SHELL_LIST]: 'Ctrl+L',
|
||||
[Command.KILL_BACKGROUND_SHELL]: 'Ctrl+K',
|
||||
[Command.UNFOCUS_BACKGROUND_SHELL]: 'Shift+Tab',
|
||||
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]: 'Tab',
|
||||
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]: 'Tab',
|
||||
[Command.FOCUS_SHELL_INPUT]: 'Focus the shell input from the gemini input.',
|
||||
[Command.UNFOCUS_SHELL_INPUT]: 'Focus the Gemini input from the shell input.',
|
||||
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
|
||||
[Command.RESTART_APP]: 'Restart the application.',
|
||||
[Command.SUSPEND_APP]: 'Suspend the application (not yet implemented).',
|
||||
};
|
||||
|
||||
@@ -164,7 +164,6 @@ describe('Policy Engine Integration Tests', () => {
|
||||
it('should handle complex mixed configurations', async () => {
|
||||
const settings: Settings = {
|
||||
tools: {
|
||||
autoAccept: true, // Allows read-only tools
|
||||
allowed: ['custom-tool', 'my-server__special-tool'],
|
||||
exclude: ['glob', 'dangerous-tool'],
|
||||
},
|
||||
@@ -438,7 +437,6 @@ describe('Policy Engine Integration Tests', () => {
|
||||
it('should verify priority ordering works correctly in practice', async () => {
|
||||
const settings: Settings = {
|
||||
tools: {
|
||||
autoAccept: true, // Priority 50
|
||||
allowed: ['specific-tool'], // Priority 100
|
||||
exclude: ['blocked-tool'], // Priority 200
|
||||
},
|
||||
@@ -614,7 +612,6 @@ describe('Policy Engine Integration Tests', () => {
|
||||
it('should verify rules are created with correct priorities', async () => {
|
||||
const settings: Settings = {
|
||||
tools: {
|
||||
autoAccept: true,
|
||||
allowed: ['tool1', 'tool2'],
|
||||
exclude: ['tool3'],
|
||||
},
|
||||
|
||||
@@ -124,7 +124,6 @@ describe('settings-validation', () => {
|
||||
},
|
||||
tools: {
|
||||
sandbox: 'inherit',
|
||||
autoAccept: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2156,7 +2156,7 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
// 2. Now, set remote admin settings.
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
secureModeEnabled: true,
|
||||
strictModeDisabled: false,
|
||||
mcpSetting: { mcpEnabled: false },
|
||||
cliFeatureSetting: { extensionsSetting: { extensionsEnabled: false } },
|
||||
});
|
||||
@@ -2197,7 +2197,7 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(loadedSettings.merged.ui?.theme).toBe('initial-theme');
|
||||
|
||||
const newRemoteSettings = {
|
||||
secureModeEnabled: true,
|
||||
strictModeDisabled: false,
|
||||
mcpSetting: { mcpEnabled: false },
|
||||
cliFeatureSetting: { extensionsSetting: { extensionsEnabled: false } },
|
||||
};
|
||||
@@ -2213,7 +2213,7 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
// Verify that calling setRemoteAdminSettings with partial data overwrites previous remote settings
|
||||
// and missing properties revert to schema defaults.
|
||||
loadedSettings.setRemoteAdminSettings({ secureModeEnabled: false });
|
||||
loadedSettings.setRemoteAdminSettings({ strictModeDisabled: true });
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false); // Defaulting to false if missing
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false); // Defaulting to false if missing
|
||||
@@ -2271,9 +2271,9 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
|
||||
|
||||
// Set remote settings with only secureModeEnabled
|
||||
// Set remote settings with only strictModeDisabled (false -> secureModeEnabled: true)
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
secureModeEnabled: true,
|
||||
strictModeDisabled: false,
|
||||
});
|
||||
|
||||
// Verify secureModeEnabled is updated, others default to false
|
||||
@@ -2286,8 +2286,8 @@ describe('Settings Loading and Merging', () => {
|
||||
mcpSetting: { mcpEnabled: false },
|
||||
});
|
||||
|
||||
// Verify mcpEnabled is updated, others remain defaults (secureModeEnabled reverts to default:false)
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
// Verify mcpEnabled is updated, others remain defaults (secureModeEnabled defaults to true if strictModeDisabled is missing)
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||
|
||||
@@ -2297,23 +2297,47 @@ describe('Settings Loading and Merging', () => {
|
||||
});
|
||||
|
||||
// Verify extensionsEnabled is updated, others remain defaults
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||
|
||||
// Verify that missing strictModeDisabled falls back to secureModeEnabled
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
secureModeEnabled: false,
|
||||
});
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
secureModeEnabled: true,
|
||||
});
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
|
||||
// Verify strictModeDisabled takes precedence over secureModeEnabled
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
strictModeDisabled: false,
|
||||
secureModeEnabled: false,
|
||||
});
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
strictModeDisabled: true,
|
||||
secureModeEnabled: true,
|
||||
});
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should set skills based on advancedFeaturesEnabled', () => {
|
||||
it('should set skills based on unmanagedCapabilitiesEnabled', () => {
|
||||
const loadedSettings = loadSettings();
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
cliFeatureSetting: {
|
||||
advancedFeaturesEnabled: true,
|
||||
unmanagedCapabilitiesEnabled: true,
|
||||
},
|
||||
});
|
||||
expect(loadedSettings.merged.admin.skills?.enabled).toBe(true);
|
||||
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
cliFeatureSetting: {
|
||||
advancedFeaturesEnabled: false,
|
||||
unmanagedCapabilitiesEnabled: false,
|
||||
},
|
||||
});
|
||||
expect(loadedSettings.merged.admin.skills?.enabled).toBe(false);
|
||||
@@ -2337,12 +2361,12 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should force secureModeEnabled to false if undefined, overriding schema defaults', () => {
|
||||
// Mock schema to have secureModeEnabled default to true to verify the override
|
||||
it('should force secureModeEnabled to true if undefined, overriding schema defaults', () => {
|
||||
// Mock schema to have secureModeEnabled default to false to verify the override
|
||||
const originalSchema = getSettingsSchema();
|
||||
const modifiedSchema = JSON.parse(JSON.stringify(originalSchema));
|
||||
if (modifiedSchema.admin?.properties?.secureModeEnabled) {
|
||||
modifiedSchema.admin.properties.secureModeEnabled.default = true;
|
||||
modifiedSchema.admin.properties.secureModeEnabled.default = false;
|
||||
}
|
||||
vi.mocked(getSettingsSchema).mockReturnValue(modifiedSchema);
|
||||
|
||||
@@ -2352,13 +2376,13 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// Pass a non-empty object that doesn't have secureModeEnabled
|
||||
// Pass a non-empty object that doesn't have strictModeDisabled
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
mcpSetting: {},
|
||||
});
|
||||
|
||||
// It should be forced to false by the logic, overriding the mock default of true
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
// It should be forced to true by the logic (default secure), overriding the mock default of false
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
} finally {
|
||||
vi.mocked(getSettingsSchema).mockReturnValue(originalSchema);
|
||||
}
|
||||
|
||||
@@ -348,7 +348,12 @@ export class LoadedSettings {
|
||||
|
||||
setRemoteAdminSettings(remoteSettings: FetchAdminControlsResponse): void {
|
||||
const admin: Settings['admin'] = {};
|
||||
const { secureModeEnabled, mcpSetting, cliFeatureSetting } = remoteSettings;
|
||||
const {
|
||||
secureModeEnabled,
|
||||
strictModeDisabled,
|
||||
mcpSetting,
|
||||
cliFeatureSetting,
|
||||
} = remoteSettings;
|
||||
|
||||
if (Object.keys(remoteSettings).length === 0) {
|
||||
this._remoteAdminSettings = { admin };
|
||||
@@ -356,15 +361,20 @@ export class LoadedSettings {
|
||||
return;
|
||||
}
|
||||
|
||||
admin.secureModeEnabled = secureModeEnabled ?? false;
|
||||
if (strictModeDisabled !== undefined) {
|
||||
admin.secureModeEnabled = !strictModeDisabled;
|
||||
} else if (secureModeEnabled !== undefined) {
|
||||
admin.secureModeEnabled = secureModeEnabled;
|
||||
} else {
|
||||
admin.secureModeEnabled = true;
|
||||
}
|
||||
admin.mcp = { enabled: mcpSetting?.mcpEnabled ?? false };
|
||||
admin.extensions = {
|
||||
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled ?? false,
|
||||
};
|
||||
|
||||
if (cliFeatureSetting?.advancedFeaturesEnabled !== undefined) {
|
||||
admin.skills = { enabled: cliFeatureSetting.advancedFeaturesEnabled };
|
||||
}
|
||||
admin.skills = {
|
||||
enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled ?? false,
|
||||
};
|
||||
|
||||
this._remoteAdminSettings = { admin };
|
||||
this._merged = this.computeMergedSettings();
|
||||
|
||||
@@ -9,19 +9,17 @@
|
||||
// to regenerate the settings reference in `docs/get-started/configuration.md`.
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
import type {
|
||||
MCPServerConfig,
|
||||
BugCommandSettings,
|
||||
TelemetrySettings,
|
||||
AuthType,
|
||||
AgentOverride,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
DEFAULT_MODEL_CONFIGS,
|
||||
type MCPServerConfig,
|
||||
type BugCommandSettings,
|
||||
type TelemetrySettings,
|
||||
type AuthType,
|
||||
type AgentOverride,
|
||||
type CustomTheme,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { CustomTheme } from '../ui/themes/theme.js';
|
||||
import type { SessionRetentionSettings } from './settings.js';
|
||||
import { DEFAULT_MIN_RETENTION } from '../utils/sessionCleanup.js';
|
||||
|
||||
@@ -526,6 +524,16 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Show the model name in the chat for each model turn.',
|
||||
showInDialog: true,
|
||||
},
|
||||
showUserIdentity: {
|
||||
type: 'boolean',
|
||||
label: 'Show User Identity',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
description:
|
||||
"Show the logged-in user's identity (e.g. email) in the UI.",
|
||||
showInDialog: true,
|
||||
},
|
||||
useAlternateBuffer: {
|
||||
type: 'boolean',
|
||||
label: 'Use Alternate Screen Buffer',
|
||||
@@ -731,6 +739,16 @@ const SETTINGS_SCHEMA = {
|
||||
'The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3).',
|
||||
showInDialog: true,
|
||||
},
|
||||
disableLoopDetection: {
|
||||
type: 'boolean',
|
||||
label: 'Disable Loop Detection',
|
||||
category: 'Model',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Disable automatic detection and prevention of infinite loops.',
|
||||
showInDialog: true,
|
||||
},
|
||||
skipNextSpeakerCheck: {
|
||||
type: 'boolean',
|
||||
label: 'Skip Next Speaker Check',
|
||||
@@ -1033,17 +1051,6 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
autoAccept: {
|
||||
type: 'boolean',
|
||||
label: 'Auto Accept',
|
||||
category: 'Tools',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: oneLine`
|
||||
Automatically accept and execute tool calls that are considered safe (e.g., read-only operations).
|
||||
`,
|
||||
showInDialog: true,
|
||||
},
|
||||
approvalMode: {
|
||||
type: 'enum',
|
||||
label: 'Approval Mode',
|
||||
@@ -1171,16 +1178,6 @@ const SETTINGS_SCHEMA = {
|
||||
`,
|
||||
showInDialog: true,
|
||||
},
|
||||
enableHooks: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Hooks System (Experimental)',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description:
|
||||
'Enables the hooks system experiment. When disabled, the hooks system is completely deactivated regardless of other settings.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1270,6 +1267,17 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Blocks installing and loading extensions from Git.',
|
||||
showInDialog: true,
|
||||
},
|
||||
allowedExtensions: {
|
||||
type: 'array',
|
||||
label: 'Extension Source Regex Allowlist',
|
||||
category: 'Security',
|
||||
requiresRestart: true,
|
||||
default: [] as string[],
|
||||
description:
|
||||
'List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting.',
|
||||
showInDialog: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
folderTrust: {
|
||||
type: 'object',
|
||||
label: 'Folder Trust',
|
||||
@@ -1490,15 +1498,6 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable Just-In-Time (JIT) context loading.',
|
||||
showInDialog: false,
|
||||
},
|
||||
skills: {
|
||||
type: 'boolean',
|
||||
label: 'Agent Skills (Deprecated)',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: '[Deprecated] Enable Agent Skills (experimental).',
|
||||
showInDialog: false,
|
||||
},
|
||||
useOSC52Paste: {
|
||||
type: 'boolean',
|
||||
label: 'Use OSC 52 Paste',
|
||||
@@ -1602,11 +1601,11 @@ const SETTINGS_SCHEMA = {
|
||||
type: 'boolean',
|
||||
label: 'Enable Hooks',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description:
|
||||
'Canonical toggle for the hooks system. When disabled, no hooks will be executed.',
|
||||
showInDialog: false,
|
||||
showInDialog: true,
|
||||
},
|
||||
disabled: {
|
||||
type: 'array',
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { loadCliConfig, parseArguments } from './config.js';
|
||||
import * as trustedFolders from './trustedFolders.js';
|
||||
import { loadServerHierarchicalMemory } from '@google/gemini-cli-core';
|
||||
import { type Settings, createTestMergedSettings } from './settings.js';
|
||||
|
||||
vi.mock('./trustedFolders.js');
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
loadServerHierarchicalMemory: vi.fn(),
|
||||
getPty: vi.fn().mockResolvedValue({ name: 'test-pty' }),
|
||||
getVersion: vi.fn().mockResolvedValue('0.0.0-test'),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Agent Skills Backward Compatibility', () => {
|
||||
const originalArgv = process.argv;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(trustedFolders.isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
} as unknown as trustedFolders.TrustResult);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = originalArgv;
|
||||
});
|
||||
|
||||
describe('loadCliConfig', () => {
|
||||
it('should default skillsSupport to true when no settings are present', async () => {
|
||||
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
process.argv = ['node', 'gemini'];
|
||||
const settings = createTestMergedSettings({});
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
'test-session',
|
||||
await parseArguments(settings),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
config as unknown as { isSkillsSupportEnabled: () => boolean }
|
||||
).isSkillsSupportEnabled(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should prioritize skills.enabled=false from settings', async () => {
|
||||
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings({
|
||||
skills: { enabled: false },
|
||||
} as unknown as Settings);
|
||||
|
||||
process.argv = ['node', 'gemini'];
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
'test-session',
|
||||
await parseArguments(settings),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
config as unknown as { isSkillsSupportEnabled: () => boolean }
|
||||
).isSkillsSupportEnabled(),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should support legacy experimental.skills=true from settings', async () => {
|
||||
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: { skills: true },
|
||||
} as unknown as Settings);
|
||||
|
||||
process.argv = ['node', 'gemini'];
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
'test-session',
|
||||
await parseArguments(settings),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
config as unknown as { isSkillsSupportEnabled: () => boolean }
|
||||
).isSkillsSupportEnabled(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should prioritize legacy experimental.skills=true over new skills.enabled=false', async () => {
|
||||
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings({
|
||||
skills: { enabled: false },
|
||||
experimental: { skills: true },
|
||||
} as unknown as Settings);
|
||||
|
||||
process.argv = ['node', 'gemini'];
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
'test-session',
|
||||
await parseArguments(settings),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
config as unknown as { isSkillsSupportEnabled: () => boolean }
|
||||
).isSkillsSupportEnabled(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should still be enabled by default if legacy experimental.skills is false (since new default is true)', async () => {
|
||||
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: { skills: false },
|
||||
} as unknown as Settings);
|
||||
|
||||
process.argv = ['node', 'gemini'];
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
'test-session',
|
||||
await parseArguments(settings),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
config as unknown as { isSkillsSupportEnabled: () => boolean }
|
||||
).isSkillsSupportEnabled(),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,11 +16,10 @@ import type { ArgumentsCamelCase, CommandModule } from 'yargs';
|
||||
import type { MergedSettings } from './config/settings.js';
|
||||
import type { MockInstance } from 'vitest';
|
||||
|
||||
const { mockRunExitCleanup, mockDebugLogger } = vi.hoisted(() => ({
|
||||
const { mockRunExitCleanup, mockCoreEvents } = vi.hoisted(() => ({
|
||||
mockRunExitCleanup: vi.fn(),
|
||||
mockDebugLogger: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
mockCoreEvents: {
|
||||
emitFeedback: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -28,7 +27,7 @@ vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actual = await vi.importActual('@google/gemini-cli-core');
|
||||
return {
|
||||
...actual,
|
||||
debugLogger: mockDebugLogger,
|
||||
coreEvents: mockCoreEvents,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -55,8 +54,7 @@ describe('deferred', () => {
|
||||
describe('runDeferredCommand', () => {
|
||||
it('should do nothing if no deferred command is set', async () => {
|
||||
await runDeferredCommand(createMockSettings());
|
||||
expect(mockDebugLogger.log).not.toHaveBeenCalled();
|
||||
expect(mockDebugLogger.error).not.toHaveBeenCalled();
|
||||
expect(mockCoreEvents.emitFeedback).not.toHaveBeenCalled();
|
||||
expect(mockExit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -85,8 +83,9 @@ describe('deferred', () => {
|
||||
const settings = createMockSettings({ mcp: { enabled: false } });
|
||||
await runDeferredCommand(settings);
|
||||
|
||||
expect(mockDebugLogger.error).toHaveBeenCalledWith(
|
||||
'Error: MCP is disabled by your admin.',
|
||||
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'MCP is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
|
||||
);
|
||||
expect(mockRunExitCleanup).toHaveBeenCalled();
|
||||
expect(mockExit).toHaveBeenCalledWith(ExitCodes.FATAL_CONFIG_ERROR);
|
||||
@@ -102,8 +101,9 @@ describe('deferred', () => {
|
||||
const settings = createMockSettings({ extensions: { enabled: false } });
|
||||
await runDeferredCommand(settings);
|
||||
|
||||
expect(mockDebugLogger.error).toHaveBeenCalledWith(
|
||||
'Error: Extensions are disabled by your admin.',
|
||||
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Extensions is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
|
||||
);
|
||||
expect(mockRunExitCleanup).toHaveBeenCalled();
|
||||
expect(mockExit).toHaveBeenCalledWith(ExitCodes.FATAL_CONFIG_ERROR);
|
||||
@@ -119,8 +119,9 @@ describe('deferred', () => {
|
||||
const settings = createMockSettings({ skills: { enabled: false } });
|
||||
await runDeferredCommand(settings);
|
||||
|
||||
expect(mockDebugLogger.error).toHaveBeenCalledWith(
|
||||
'Error: Agent skills are disabled by your admin.',
|
||||
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Agent skills is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
|
||||
);
|
||||
expect(mockRunExitCleanup).toHaveBeenCalled();
|
||||
expect(mockExit).toHaveBeenCalledWith(ExitCodes.FATAL_CONFIG_ERROR);
|
||||
@@ -183,8 +184,9 @@ describe('deferred', () => {
|
||||
const mcpSettings = createMockSettings({ mcp: { enabled: false } });
|
||||
await runDeferredCommand(mcpSettings);
|
||||
|
||||
expect(mockDebugLogger.error).toHaveBeenCalledWith(
|
||||
'Error: MCP is disabled by your admin.',
|
||||
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'MCP is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type { ArgumentsCamelCase, CommandModule } from 'yargs';
|
||||
import { debugLogger, ExitCodes } from '@google/gemini-cli-core';
|
||||
import {
|
||||
coreEvents,
|
||||
ExitCodes,
|
||||
getAdminErrorMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { runExitCleanup } from './utils/cleanup.js';
|
||||
import type { MergedSettings } from './config/settings.js';
|
||||
import process from 'node:process';
|
||||
@@ -30,7 +34,10 @@ export async function runDeferredCommand(settings: MergedSettings) {
|
||||
const commandName = deferredCommand.commandName;
|
||||
|
||||
if (commandName === 'mcp' && adminSettings?.mcp?.enabled === false) {
|
||||
debugLogger.error('Error: MCP is disabled by your admin.');
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
getAdminErrorMessage('MCP', undefined /* config */),
|
||||
);
|
||||
await runExitCleanup();
|
||||
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
|
||||
}
|
||||
@@ -39,13 +46,19 @@ export async function runDeferredCommand(settings: MergedSettings) {
|
||||
commandName === 'extensions' &&
|
||||
adminSettings?.extensions?.enabled === false
|
||||
) {
|
||||
debugLogger.error('Error: Extensions are disabled by your admin.');
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
getAdminErrorMessage('Extensions', undefined /* config */),
|
||||
);
|
||||
await runExitCleanup();
|
||||
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
|
||||
}
|
||||
|
||||
if (commandName === 'skills' && adminSettings?.skills?.enabled === false) {
|
||||
debugLogger.error('Error: Agent skills are disabled by your admin.');
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
getAdminErrorMessage('Agent skills', undefined /* config */),
|
||||
);
|
||||
await runExitCleanup();
|
||||
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ import {
|
||||
runExitCleanup,
|
||||
registerTelemetryConfig,
|
||||
} from './utils/cleanup.js';
|
||||
import {
|
||||
cleanupToolOutputFiles,
|
||||
cleanupExpiredSessions,
|
||||
} from './utils/sessionCleanup.js';
|
||||
import {
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
@@ -71,7 +75,6 @@ import {
|
||||
} from './core/initializer.js';
|
||||
import { validateAuthMethod } from './config/auth.js';
|
||||
import { runZedIntegration } from './zed-integration/zedIntegration.js';
|
||||
import { cleanupExpiredSessions } from './utils/sessionCleanup.js';
|
||||
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
|
||||
import { checkForUpdates } from './ui/utils/updateCheck.js';
|
||||
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
|
||||
@@ -320,7 +323,10 @@ export async function main() {
|
||||
);
|
||||
});
|
||||
|
||||
await cleanupCheckpoints();
|
||||
await Promise.all([
|
||||
cleanupCheckpoints(),
|
||||
cleanupToolOutputFiles(settings.merged),
|
||||
]);
|
||||
|
||||
const parseArgsHandle = startupProfiler.start('parse_arguments');
|
||||
const argv = await parseArguments(settings.merged);
|
||||
@@ -367,7 +373,7 @@ export async function main() {
|
||||
) {
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
'selectedAuthType',
|
||||
'security.auth.selectedType',
|
||||
AuthType.COMPUTE_ADC,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ import type { LoadedSettings } from './config/settings.js';
|
||||
// Mock core modules
|
||||
vi.mock('./ui/hooks/atCommandProcessor.js');
|
||||
|
||||
const mockRegisterActivityLogger = vi.hoisted(() => vi.fn());
|
||||
vi.mock('./utils/activityLogger.js', () => ({
|
||||
registerActivityLogger: mockRegisterActivityLogger,
|
||||
}));
|
||||
|
||||
const mockCoreEvents = vi.hoisted(() => ({
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
@@ -253,12 +258,61 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Test input' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-1',
|
||||
undefined,
|
||||
false,
|
||||
'Test input',
|
||||
);
|
||||
expect(getWrittenOutput()).toBe('Hello World\n');
|
||||
// Note: Telemetry shutdown is now handled in runExitCleanup() in cleanup.ts
|
||||
// so we no longer expect shutdownTelemetry to be called directly here
|
||||
});
|
||||
|
||||
it('should register activity logger when GEMINI_CLI_ACTIVITY_LOG_FILE is set', async () => {
|
||||
vi.stubEnv('GEMINI_CLI_ACTIVITY_LOG_FILE', '/tmp/test.jsonl');
|
||||
const events: ServerGeminiStreamEvent[] = [
|
||||
{
|
||||
type: GeminiEventType.Finished,
|
||||
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
||||
},
|
||||
];
|
||||
mockGeminiClient.sendMessageStream.mockReturnValue(
|
||||
createStreamFromEvents(events),
|
||||
);
|
||||
|
||||
await runNonInteractive({
|
||||
config: mockConfig,
|
||||
settings: mockSettings,
|
||||
input: 'test',
|
||||
prompt_id: 'prompt-id-activity-logger',
|
||||
});
|
||||
|
||||
expect(mockRegisterActivityLogger).toHaveBeenCalledWith(mockConfig);
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should not register activity logger when GEMINI_CLI_ACTIVITY_LOG_FILE is not set', async () => {
|
||||
vi.stubEnv('GEMINI_CLI_ACTIVITY_LOG_FILE', '');
|
||||
const events: ServerGeminiStreamEvent[] = [
|
||||
{
|
||||
type: GeminiEventType.Finished,
|
||||
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
||||
},
|
||||
];
|
||||
mockGeminiClient.sendMessageStream.mockReturnValue(
|
||||
createStreamFromEvents(events),
|
||||
);
|
||||
|
||||
await runNonInteractive({
|
||||
config: mockConfig,
|
||||
settings: mockSettings,
|
||||
input: 'test',
|
||||
prompt_id: 'prompt-id-activity-logger-off',
|
||||
});
|
||||
|
||||
expect(mockRegisterActivityLogger).not.toHaveBeenCalled();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should handle a single tool call and respond', async () => {
|
||||
const toolCallEvent: ServerGeminiStreamEvent = {
|
||||
type: GeminiEventType.ToolCallRequest,
|
||||
@@ -323,6 +377,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Tool response' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-2',
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
expect(getWrittenOutput()).toBe('Final answer\n');
|
||||
});
|
||||
@@ -480,6 +537,9 @@ describe('runNonInteractive', () => {
|
||||
],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-3',
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
expect(getWrittenOutput()).toBe('Sorry, let me try again.\n');
|
||||
});
|
||||
@@ -619,6 +679,9 @@ describe('runNonInteractive', () => {
|
||||
processedParts,
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-7',
|
||||
undefined,
|
||||
false,
|
||||
rawInput,
|
||||
);
|
||||
|
||||
// 6. Assert the final output is correct
|
||||
@@ -652,6 +715,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Test input' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-1',
|
||||
undefined,
|
||||
false,
|
||||
'Test input',
|
||||
);
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
@@ -782,6 +848,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Empty response test' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-empty',
|
||||
undefined,
|
||||
false,
|
||||
'Empty response test',
|
||||
);
|
||||
|
||||
// This should output JSON with empty response but include stats
|
||||
@@ -916,6 +985,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Prompt from command' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-slash',
|
||||
undefined,
|
||||
false,
|
||||
'/testcommand',
|
||||
);
|
||||
|
||||
expect(getWrittenOutput()).toBe('Response from command\n');
|
||||
@@ -959,6 +1031,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Slash command output' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-slash',
|
||||
undefined,
|
||||
false,
|
||||
'/help',
|
||||
);
|
||||
expect(getWrittenOutput()).toBe('Response to slash command\n');
|
||||
handleSlashCommandSpy.mockRestore();
|
||||
@@ -1133,6 +1208,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: '/unknowncommand' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-unknown',
|
||||
undefined,
|
||||
false,
|
||||
'/unknowncommand',
|
||||
);
|
||||
|
||||
expect(getWrittenOutput()).toBe('Response to unknown\n');
|
||||
|
||||
@@ -70,6 +70,14 @@ export async function runNonInteractive({
|
||||
coreEvents.emitConsoleLog(msg.type, msg.content);
|
||||
},
|
||||
});
|
||||
|
||||
if (config.storage && process.env['GEMINI_CLI_ACTIVITY_LOG_FILE']) {
|
||||
const { registerActivityLogger } = await import(
|
||||
'./utils/activityLogger.js'
|
||||
);
|
||||
registerActivityLogger(config);
|
||||
}
|
||||
|
||||
const { stdout: workingStdout } = createWorkingStdio();
|
||||
const textOutput = new TextOutput(workingStdout);
|
||||
|
||||
@@ -293,6 +301,9 @@ export async function runNonInteractive({
|
||||
currentMessages[0]?.parts || [],
|
||||
abortController.signal,
|
||||
prompt_id,
|
||||
undefined,
|
||||
false,
|
||||
turnCount === 1 ? input : undefined,
|
||||
);
|
||||
|
||||
let responseText = '';
|
||||
|
||||
@@ -119,11 +119,12 @@ describe('BuiltinCommandLoader', () => {
|
||||
getEnableHooks: () => false,
|
||||
getEnableHooksUI: () => false,
|
||||
getExtensionsEnabled: vi.fn().mockReturnValue(true),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(true),
|
||||
isAgentsEnabled: vi.fn().mockReturnValue(false),
|
||||
getMcpEnabled: vi.fn().mockReturnValue(true),
|
||||
getSkillManager: vi.fn().mockReturnValue({
|
||||
getAllSkills: vi.fn().mockReturnValue([]),
|
||||
isAdminEnabled: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
} as unknown as Config;
|
||||
|
||||
@@ -260,11 +261,12 @@ describe('BuiltinCommandLoader profile', () => {
|
||||
getEnableHooks: () => false,
|
||||
getEnableHooksUI: () => false,
|
||||
getExtensionsEnabled: vi.fn().mockReturnValue(true),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(true),
|
||||
isAgentsEnabled: vi.fn().mockReturnValue(false),
|
||||
getMcpEnabled: vi.fn().mockReturnValue(true),
|
||||
getSkillManager: vi.fn().mockReturnValue({
|
||||
getAllSkills: vi.fn().mockReturnValue([]),
|
||||
isAdminEnabled: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
@@ -12,7 +12,11 @@ import {
|
||||
type CommandContext,
|
||||
} from '../ui/commands/types.js';
|
||||
import type { MessageActionReturn, Config } from '@google/gemini-cli-core';
|
||||
import { isNightly, startupProfiler } from '@google/gemini-cli-core';
|
||||
import {
|
||||
isNightly,
|
||||
startupProfiler,
|
||||
getAdminErrorMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { aboutCommand } from '../ui/commands/aboutCommand.js';
|
||||
import { agentsCommand } from '../ui/commands/agentsCommand.js';
|
||||
import { authCommand } from '../ui/commands/authCommand.js';
|
||||
@@ -47,6 +51,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 { 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';
|
||||
@@ -101,7 +106,10 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
): Promise<MessageActionReturn> => ({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Extensions are disabled by your admin.',
|
||||
content: getAdminErrorMessage(
|
||||
'Extensions',
|
||||
this.config ?? undefined,
|
||||
),
|
||||
}),
|
||||
},
|
||||
]
|
||||
@@ -126,7 +134,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
): Promise<MessageActionReturn> => ({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'MCP is disabled by your admin.',
|
||||
content: getAdminErrorMessage('MCP', this.config ?? undefined),
|
||||
}),
|
||||
},
|
||||
]
|
||||
@@ -157,13 +165,17 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
): Promise<MessageActionReturn> => ({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Agent skills are disabled by your admin.',
|
||||
content: getAdminErrorMessage(
|
||||
'Agent skills',
|
||||
this.config ?? undefined,
|
||||
),
|
||||
}),
|
||||
},
|
||||
]
|
||||
: [skillsCommand]
|
||||
: []),
|
||||
settingsCommand,
|
||||
shellsCommand,
|
||||
vimCommand,
|
||||
setupGithubCommand,
|
||||
terminalSetupCommand,
|
||||
|
||||
@@ -9,12 +9,13 @@ import * as path from 'node:path';
|
||||
import {
|
||||
type MCPServerConfig,
|
||||
type ExtensionInstallMetadata,
|
||||
type ExtensionSetting,
|
||||
type CustomTheme,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
EXTENSIONS_CONFIG_FILENAME,
|
||||
INSTALL_METADATA_FILENAME,
|
||||
} from '../config/extensions/variables.js';
|
||||
import type { ExtensionSetting } from '../config/extensions/extensionSettings.js';
|
||||
|
||||
export function createExtension({
|
||||
extensionsDir = 'extensions-dir',
|
||||
@@ -25,12 +26,20 @@ export function createExtension({
|
||||
mcpServers = {} as Record<string, MCPServerConfig>,
|
||||
installMetadata = undefined as ExtensionInstallMetadata | undefined,
|
||||
settings = undefined as ExtensionSetting[] | undefined,
|
||||
themes = undefined as CustomTheme[] | undefined,
|
||||
} = {}): string {
|
||||
const extDir = path.join(extensionsDir, name);
|
||||
fs.mkdirSync(extDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
|
||||
JSON.stringify({ name, version, contextFileName, mcpServers, settings }),
|
||||
JSON.stringify({
|
||||
name,
|
||||
version,
|
||||
contextFileName,
|
||||
mcpServers,
|
||||
settings,
|
||||
themes,
|
||||
}),
|
||||
);
|
||||
|
||||
if (addContextFile) {
|
||||
|
||||
@@ -157,6 +157,9 @@ const baseMockUiState = {
|
||||
terminalHeight: 40,
|
||||
currentModel: 'gemini-pro',
|
||||
terminalBackgroundColor: undefined,
|
||||
activePtyId: undefined,
|
||||
backgroundShells: new Map(),
|
||||
backgroundShellHeight: 0,
|
||||
};
|
||||
|
||||
export const mockAppState: AppState = {
|
||||
@@ -201,7 +204,11 @@ const mockUIActions: UIActions = {
|
||||
handleApiKeyCancel: vi.fn(),
|
||||
setBannerVisible: vi.fn(),
|
||||
setEmbeddedShellFocused: vi.fn(),
|
||||
dismissBackgroundShell: vi.fn(),
|
||||
setActiveBackgroundShellPid: vi.fn(),
|
||||
setIsBackgroundShellListOpen: vi.fn(),
|
||||
setAuthContext: vi.fn(),
|
||||
handleWarning: vi.fn(),
|
||||
handleRestart: vi.fn(),
|
||||
handleNewAgentsSelect: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -88,6 +88,7 @@ describe('App', () => {
|
||||
defaultText: 'Mock Banner Text',
|
||||
warningText: '',
|
||||
},
|
||||
backgroundShells: new Map(),
|
||||
};
|
||||
|
||||
it('should render main content and composer when not quitting', () => {
|
||||
@@ -234,7 +235,6 @@ describe('App', () => {
|
||||
expect(lastFrame()).toContain('Tips for getting started');
|
||||
expect(lastFrame()).toContain('Notifications');
|
||||
expect(lastFrame()).toContain('Action Required'); // From ToolConfirmationQueue
|
||||
expect(lastFrame()).toContain('1 of 1');
|
||||
expect(lastFrame()).toContain('Composer');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import { act, useContext, type ReactElement } from 'react';
|
||||
import { AppContainer } from './AppContainer.js';
|
||||
import { SettingsContext } from './contexts/SettingsContext.js';
|
||||
import { type TrackedToolCall } from './hooks/useReactToolScheduler.js';
|
||||
import { MessageType } from './types.js';
|
||||
import {
|
||||
type Config,
|
||||
makeFakeConfig,
|
||||
@@ -28,6 +29,8 @@ import {
|
||||
type UserFeedbackPayload,
|
||||
type ResumedSessionData,
|
||||
AuthType,
|
||||
UserAccountManager,
|
||||
type ContentGeneratorConfig,
|
||||
type AgentDefinition,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
@@ -44,6 +47,11 @@ const mockIdeClient = vi.hoisted(() => ({
|
||||
getInstance: vi.fn().mockReturnValue(new Promise(() => {})),
|
||||
}));
|
||||
|
||||
// Mock UserAccountManager
|
||||
const mockUserAccountManager = vi.hoisted(() => ({
|
||||
getCachedGoogleAccount: vi.fn().mockReturnValue(null),
|
||||
}));
|
||||
|
||||
// Mock stdout
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockStdout: { write: vi.fn() },
|
||||
@@ -73,6 +81,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
})),
|
||||
enableMouseEvents: vi.fn(),
|
||||
disableMouseEvents: vi.fn(),
|
||||
UserAccountManager: vi
|
||||
.fn()
|
||||
.mockImplementation(() => mockUserAccountManager),
|
||||
FileDiscoveryService: vi.fn().mockImplementation(() => ({
|
||||
initialize: vi.fn(),
|
||||
})),
|
||||
@@ -185,6 +196,7 @@ import {
|
||||
disableMouseEvents,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type ExtensionManager } from '../config/extension-manager.js';
|
||||
import { WARNING_PROMPT_DURATION_MS } from './constants.js';
|
||||
|
||||
describe('AppContainer State Management', () => {
|
||||
let mockConfig: Config;
|
||||
@@ -249,6 +261,25 @@ describe('AppContainer State Management', () => {
|
||||
const mockedUseInputHistoryStore = useInputHistoryStore as Mock;
|
||||
const mockedUseHookDisplayState = useHookDisplayState as Mock;
|
||||
|
||||
const DEFAULT_GEMINI_STREAM_MOCK = {
|
||||
streamingState: 'idle',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
handleApprovalModeChange: vi.fn(),
|
||||
activePtyId: null,
|
||||
loopDetectionConfirmationRequest: null,
|
||||
backgroundShellCount: 0,
|
||||
isBackgroundShellVisible: false,
|
||||
toggleBackgroundShell: vi.fn(),
|
||||
backgroundCurrentShell: vi.fn(),
|
||||
backgroundShells: new Map(),
|
||||
registerBackgroundShell: vi.fn(),
|
||||
dismissBackgroundShell: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -313,14 +344,7 @@ describe('AppContainer State Management', () => {
|
||||
handleNewMessage: vi.fn(),
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
streamingState: 'idle',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
});
|
||||
mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK);
|
||||
mockedUseVim.mockReturnValue({ handleInput: vi.fn() });
|
||||
mockedUseFolderTrust.mockReturnValue({
|
||||
isFolderTrustDialogOpen: false,
|
||||
@@ -347,7 +371,9 @@ describe('AppContainer State Management', () => {
|
||||
mockedUseTextBuffer.mockReturnValue({
|
||||
text: '',
|
||||
setText: vi.fn(),
|
||||
// Add other properties if AppContainer uses them
|
||||
lines: [''],
|
||||
cursor: [0, 0],
|
||||
handleInput: vi.fn().mockReturnValue(false),
|
||||
});
|
||||
mockedUseLogger.mockReturnValue({
|
||||
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
|
||||
@@ -395,6 +421,7 @@ describe('AppContainer State Management', () => {
|
||||
...defaultMergedSettings.ui,
|
||||
showStatusInTitle: false,
|
||||
hideWindowTitle: false,
|
||||
showUserIdentity: true,
|
||||
},
|
||||
useAlternateBuffer: false,
|
||||
},
|
||||
@@ -466,6 +493,162 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authentication Check', () => {
|
||||
it('displays correct message for LOGIN_WITH_GOOGLE auth type', async () => {
|
||||
// Explicitly mock implementation to ensure we control the instance
|
||||
(UserAccountManager as unknown as Mock).mockImplementation(
|
||||
() => mockUserAccountManager,
|
||||
);
|
||||
|
||||
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(
|
||||
'test@example.com',
|
||||
);
|
||||
const mockAddItem = vi.fn();
|
||||
mockedUseHistory.mockReturnValue({
|
||||
history: [],
|
||||
addItem: mockAddItem,
|
||||
updateItem: vi.fn(),
|
||||
clearItems: vi.fn(),
|
||||
loadHistory: vi.fn(),
|
||||
});
|
||||
|
||||
// Explicitly enable showUserIdentity
|
||||
mockSettings.merged.ui = {
|
||||
...mockSettings.merged.ui,
|
||||
showUserIdentity: true,
|
||||
};
|
||||
|
||||
// Need to ensure config.getContentGeneratorConfig() returns appropriate authType
|
||||
const authConfig = makeFakeConfig();
|
||||
// Mock getTargetDir as well since makeFakeConfig might not set it up fully for the component
|
||||
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
|
||||
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
|
||||
mockExtensionManager,
|
||||
);
|
||||
|
||||
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
} as unknown as ContentGeneratorConfig);
|
||||
vi.spyOn(authConfig, 'getUserTierName').mockReturnValue('Standard Tier');
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ config: authConfig });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(UserAccountManager).toHaveBeenCalled();
|
||||
expect(
|
||||
mockUserAccountManager.getCachedGoogleAccount,
|
||||
).toHaveBeenCalled();
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: 'Logged in with Google: test@example.com (Plan: Standard Tier)',
|
||||
}),
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
it('displays correct message for USE_GEMINI auth type', async () => {
|
||||
// Explicitly mock implementation to ensure we control the instance
|
||||
(UserAccountManager as unknown as Mock).mockImplementation(
|
||||
() => mockUserAccountManager,
|
||||
);
|
||||
|
||||
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(null);
|
||||
const mockAddItem = vi.fn();
|
||||
mockedUseHistory.mockReturnValue({
|
||||
history: [],
|
||||
addItem: mockAddItem,
|
||||
updateItem: vi.fn(),
|
||||
clearItems: vi.fn(),
|
||||
loadHistory: vi.fn(),
|
||||
});
|
||||
|
||||
const authConfig = makeFakeConfig();
|
||||
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
|
||||
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
|
||||
mockExtensionManager,
|
||||
);
|
||||
|
||||
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.USE_GEMINI,
|
||||
} as unknown as ContentGeneratorConfig);
|
||||
vi.spyOn(authConfig, 'getUserTierName').mockReturnValue('Standard Tier');
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ config: authConfig });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: expect.stringContaining('Authenticated with gemini-api-key'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not display authentication message if showUserIdentity is false', async () => {
|
||||
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(
|
||||
'test@example.com',
|
||||
);
|
||||
const mockAddItem = vi.fn();
|
||||
mockedUseHistory.mockReturnValue({
|
||||
history: [],
|
||||
addItem: mockAddItem,
|
||||
updateItem: vi.fn(),
|
||||
clearItems: vi.fn(),
|
||||
loadHistory: vi.fn(),
|
||||
});
|
||||
|
||||
mockSettings.merged.ui = {
|
||||
...mockSettings.merged.ui,
|
||||
showUserIdentity: false,
|
||||
};
|
||||
|
||||
const authConfig = makeFakeConfig();
|
||||
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
|
||||
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
|
||||
mockExtensionManager,
|
||||
);
|
||||
|
||||
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
} as unknown as ContentGeneratorConfig);
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ config: authConfig });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
|
||||
// Give it some time to potentially call addItem
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(mockAddItem).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
}),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Context Providers', () => {
|
||||
it('provides AppContext with correct values', async () => {
|
||||
let unmount: () => void;
|
||||
@@ -1015,12 +1198,9 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Mock the streaming state as Active
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: { subject: 'Some thought' },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
@@ -1056,12 +1236,9 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Mock the streaming state
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: { subject: 'Some thought' },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
@@ -1128,12 +1305,9 @@ describe('AppContainer State Management', () => {
|
||||
// Mock the streaming state and thought
|
||||
const thoughtSubject = 'Processing request';
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: { subject: thoughtSubject },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
@@ -1169,14 +1343,7 @@ describe('AppContainer State Management', () => {
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
// Mock the streaming state as Idle with no thought
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
streamingState: 'idle',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
});
|
||||
mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK);
|
||||
|
||||
// Act: Render the container
|
||||
const { unmount } = renderAppContainer({
|
||||
@@ -1213,12 +1380,9 @@ describe('AppContainer State Management', () => {
|
||||
// Mock the streaming state and thought
|
||||
const thoughtSubject = 'Confirm tool execution';
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'waiting_for_confirmation',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: { subject: thoughtSubject },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
@@ -1270,16 +1434,11 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Mock an active shell pty but not focused
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: { subject: 'Executing shell command' },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
pendingToolCalls: [],
|
||||
handleApprovalModeChange: vi.fn(),
|
||||
activePtyId: 'pty-1',
|
||||
loopDetectionConfirmationRequest: null,
|
||||
lastOutputTime: startTime + 100, // Trigger aggressive delay
|
||||
retryStatus: null,
|
||||
});
|
||||
@@ -1334,12 +1493,9 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Mock an active shell pty with redirection active
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: { subject: 'Executing shell command' },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
pendingToolCalls: [
|
||||
{
|
||||
request: {
|
||||
@@ -1349,9 +1505,7 @@ describe('AppContainer State Management', () => {
|
||||
status: 'executing',
|
||||
} as unknown as TrackedToolCall,
|
||||
],
|
||||
handleApprovalModeChange: vi.fn(),
|
||||
activePtyId: 'pty-1',
|
||||
loopDetectionConfirmationRequest: null,
|
||||
lastOutputTime: startTime,
|
||||
retryStatus: null,
|
||||
});
|
||||
@@ -1409,16 +1563,11 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Mock an active shell pty with NO output since operation started (silent)
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: { subject: 'Executing shell command' },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
pendingToolCalls: [],
|
||||
handleApprovalModeChange: vi.fn(),
|
||||
activePtyId: 'pty-1',
|
||||
loopDetectionConfirmationRequest: null,
|
||||
lastOutputTime: startTime, // lastOutputTime <= operationStartTime
|
||||
retryStatus: null,
|
||||
});
|
||||
@@ -1465,12 +1614,9 @@ describe('AppContainer State Management', () => {
|
||||
// Mock an active shell pty but not focused
|
||||
let lastOutputTime = startTime + 1000;
|
||||
mockedUseGeminiStream.mockImplementation(() => ({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: { subject: 'Executing shell command' },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
activePtyId: 'pty-1',
|
||||
lastOutputTime,
|
||||
}));
|
||||
@@ -1491,12 +1637,9 @@ describe('AppContainer State Management', () => {
|
||||
// Update lastOutputTime to simulate new output
|
||||
lastOutputTime = startTime + 21000;
|
||||
mockedUseGeminiStream.mockImplementation(() => ({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: { subject: 'Executing shell command' },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
activePtyId: 'pty-1',
|
||||
lastOutputTime,
|
||||
}));
|
||||
@@ -1556,12 +1699,9 @@ describe('AppContainer State Management', () => {
|
||||
// Mock the streaming state and thought with a short subject
|
||||
const shortTitle = 'Short';
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: { subject: shortTitle },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
@@ -1600,12 +1740,9 @@ describe('AppContainer State Management', () => {
|
||||
// Mock the streaming state and thought
|
||||
const title = 'Test Title';
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: { subject: title },
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
@@ -1643,12 +1780,8 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Mock the streaming state
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
@@ -1750,12 +1883,7 @@ describe('AppContainer State Management', () => {
|
||||
mockedMeasureElement.mockReturnValue({ width: 80, height: 10 }); // Footer is taller than the screen
|
||||
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
streamingState: 'idle',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
activePtyId: 'some-id',
|
||||
});
|
||||
|
||||
@@ -1774,7 +1902,7 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
describe('Keyboard Input Handling (CTRL+C / CTRL+D)', () => {
|
||||
let handleGlobalKeypress: (key: Key) => void;
|
||||
let handleGlobalKeypress: (key: Key) => boolean;
|
||||
let mockHandleSlashCommand: Mock;
|
||||
let mockCancelOngoingRequest: Mock;
|
||||
let rerender: () => void;
|
||||
@@ -1809,9 +1937,11 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// Capture the keypress handler from the AppContainer
|
||||
mockedUseKeypress.mockImplementation((callback: (key: Key) => void) => {
|
||||
handleGlobalKeypress = callback;
|
||||
});
|
||||
mockedUseKeypress.mockImplementation(
|
||||
(callback: (key: Key) => boolean) => {
|
||||
handleGlobalKeypress = callback;
|
||||
},
|
||||
);
|
||||
|
||||
// Mock slash command handler
|
||||
mockHandleSlashCommand = vi.fn();
|
||||
@@ -1827,11 +1957,7 @@ describe('AppContainer State Management', () => {
|
||||
// Mock request cancellation
|
||||
mockCancelOngoingRequest = vi.fn();
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
streamingState: 'idle',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
cancelOngoingRequest: mockCancelOngoingRequest,
|
||||
});
|
||||
|
||||
@@ -1839,6 +1965,9 @@ describe('AppContainer State Management', () => {
|
||||
mockedUseTextBuffer.mockReturnValue({
|
||||
text: '',
|
||||
setText: vi.fn(),
|
||||
lines: [''],
|
||||
cursor: [0, 0],
|
||||
handleInput: vi.fn().mockReturnValue(false),
|
||||
});
|
||||
|
||||
vi.useFakeTimers();
|
||||
@@ -1852,11 +1981,8 @@ describe('AppContainer State Management', () => {
|
||||
describe('CTRL+C', () => {
|
||||
it('should cancel ongoing request on first press', async () => {
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
streamingState: 'responding',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: mockCancelOngoingRequest,
|
||||
});
|
||||
await setupKeypressTest();
|
||||
@@ -1891,7 +2017,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Advance timer past the reset threshold
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1001);
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
|
||||
});
|
||||
|
||||
pressKey({ name: 'c', ctrl: true });
|
||||
@@ -1901,19 +2027,6 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
describe('CTRL+D', () => {
|
||||
it('should do nothing if text buffer is not empty', async () => {
|
||||
mockedUseTextBuffer.mockReturnValue({
|
||||
text: 'some text',
|
||||
setText: vi.fn(),
|
||||
});
|
||||
await setupKeypressTest();
|
||||
|
||||
pressKey({ name: 'd', ctrl: true }, 2);
|
||||
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should quit on second press if buffer is empty', async () => {
|
||||
await setupKeypressTest();
|
||||
|
||||
@@ -1928,6 +2041,50 @@ describe('AppContainer State Management', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT quit if buffer is not empty (bubbles from InputPrompt)', async () => {
|
||||
mockedUseTextBuffer.mockReturnValue({
|
||||
text: 'some text',
|
||||
setText: vi.fn(),
|
||||
lines: ['some text'],
|
||||
cursor: [0, 9], // At the end
|
||||
handleInput: vi.fn().mockReturnValue(false),
|
||||
});
|
||||
await setupKeypressTest();
|
||||
|
||||
// Capture return value
|
||||
let result = true;
|
||||
const originalPressKey = (key: Partial<Key>) => {
|
||||
act(() => {
|
||||
result = handleGlobalKeypress({
|
||||
name: 'd',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: true,
|
||||
cmd: false,
|
||||
...key,
|
||||
} as Key);
|
||||
});
|
||||
rerender();
|
||||
};
|
||||
|
||||
originalPressKey({ name: 'd', ctrl: true });
|
||||
|
||||
// AppContainer's handler should return true if it reaches it
|
||||
expect(result).toBe(true);
|
||||
// But it should only be called once, so count is 1, not quitting yet.
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
|
||||
originalPressKey({ name: 'd', ctrl: true });
|
||||
// Now count is 2, it should quit.
|
||||
expect(mockHandleSlashCommand).toHaveBeenCalledWith(
|
||||
'/quit',
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should reset press count after a timeout', async () => {
|
||||
await setupKeypressTest();
|
||||
|
||||
@@ -1936,7 +2093,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Advance timer past the reset threshold
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1001);
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
|
||||
});
|
||||
|
||||
pressKey({ name: 'd', ctrl: true });
|
||||
@@ -1947,7 +2104,7 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
describe('Copy Mode (CTRL+S)', () => {
|
||||
let handleGlobalKeypress: (key: Key) => void;
|
||||
let handleGlobalKeypress: (key: Key) => boolean;
|
||||
let rerender: () => void;
|
||||
let unmount: () => void;
|
||||
|
||||
@@ -1977,9 +2134,11 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.mockStdout.write.mockClear();
|
||||
mockedUseKeypress.mockImplementation((callback: (key: Key) => void) => {
|
||||
handleGlobalKeypress = callback;
|
||||
});
|
||||
mockedUseKeypress.mockImplementation(
|
||||
(callback: (key: Key) => boolean) => {
|
||||
handleGlobalKeypress = callback;
|
||||
},
|
||||
);
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
@@ -2332,6 +2491,59 @@ describe('AppContainer State Management', () => {
|
||||
expect(capturedUIState.activeHooks).toEqual(mockHooks);
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('handles consent request events', async () => {
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
const handler = mockCoreEvents.on.mock.calls.find(
|
||||
(call: unknown[]) => call[0] === CoreEvent.ConsentRequest,
|
||||
)?.[1];
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
const onConfirm = vi.fn();
|
||||
const payload = {
|
||||
prompt: 'Do you consent?',
|
||||
onConfirm,
|
||||
};
|
||||
|
||||
act(() => {
|
||||
handler(payload);
|
||||
});
|
||||
|
||||
expect(capturedUIState.authConsentRequest).toBeDefined();
|
||||
expect(capturedUIState.authConsentRequest?.prompt).toBe(
|
||||
'Do you consent?',
|
||||
);
|
||||
|
||||
act(() => {
|
||||
capturedUIState.authConsentRequest?.onConfirm(true);
|
||||
});
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(true);
|
||||
expect(capturedUIState.authConsentRequest).toBeNull();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('unsubscribes from ConsentRequest on unmount', async () => {
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
unmount!();
|
||||
|
||||
expect(mockCoreEvents.off).toHaveBeenCalledWith(
|
||||
CoreEvent.ConsentRequest,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shell Interaction', () => {
|
||||
@@ -2343,12 +2555,7 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
mockedUseGeminiStream.mockReturnValue({
|
||||
streamingState: 'idle',
|
||||
submitQuery: vi.fn(),
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
thought: null,
|
||||
cancelOngoingRequest: vi.fn(),
|
||||
...DEFAULT_GEMINI_STREAM_MOCK,
|
||||
activePtyId: 'some-pty-id', // Make sure activePtyId is set
|
||||
});
|
||||
|
||||
|
||||
@@ -27,13 +27,10 @@ import {
|
||||
type HistoryItemWithoutId,
|
||||
type HistoryItemToolGroup,
|
||||
AuthState,
|
||||
type ConfirmationRequest,
|
||||
} from './types.js';
|
||||
import { MessageType, StreamingState } from './types.js';
|
||||
import { ToolActionsProvider } from './contexts/ToolActionsContext.js';
|
||||
import {
|
||||
AskUserActionsProvider,
|
||||
type AskUserState,
|
||||
} from './contexts/AskUserActionsContext.js';
|
||||
import {
|
||||
type EditorType,
|
||||
type Config,
|
||||
@@ -47,6 +44,7 @@ import {
|
||||
getErrorMessage,
|
||||
getAllGeminiMdFilenames,
|
||||
AuthType,
|
||||
UserAccountManager,
|
||||
clearCachedCredentialFile,
|
||||
type ResumedSessionData,
|
||||
recordExitFail,
|
||||
@@ -67,8 +65,7 @@ import {
|
||||
SessionStartSource,
|
||||
SessionEndReason,
|
||||
generateSummary,
|
||||
MessageBusType,
|
||||
type AskUserRequest,
|
||||
type ConsentRequestPayload,
|
||||
type AgentsDiscoveredPayload,
|
||||
ChangeAuthRequestedError,
|
||||
} from '@google/gemini-cli-core';
|
||||
@@ -96,6 +93,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 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';
|
||||
@@ -135,6 +133,7 @@ import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js'
|
||||
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
|
||||
import { useBanner } from './hooks/useBanner.js';
|
||||
import { useHookDisplayState } from './hooks/useHookDisplayState.js';
|
||||
import { useBackgroundShellManager } from './hooks/useBackgroundShellManager.js';
|
||||
import {
|
||||
WARNING_PROMPT_DURATION_MS,
|
||||
QUEUE_ERROR_DISPLAY_DURATION_MS,
|
||||
@@ -186,11 +185,58 @@ const SHELL_HEIGHT_PADDING = 10;
|
||||
|
||||
export const AppContainer = (props: AppContainerProps) => {
|
||||
const { config, initializationResult, resumedSessionData } = props;
|
||||
const settings = useSettings();
|
||||
|
||||
const historyManager = useHistory({
|
||||
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
|
||||
});
|
||||
const { addItem } = historyManager;
|
||||
|
||||
const authCheckPerformed = useRef(false);
|
||||
useEffect(() => {
|
||||
if (authCheckPerformed.current) return;
|
||||
authCheckPerformed.current = true;
|
||||
|
||||
if (resumedSessionData || settings.merged.ui.showUserIdentity === false) {
|
||||
return;
|
||||
}
|
||||
const authType = config.getContentGeneratorConfig()?.authType;
|
||||
|
||||
// Run this asynchronously to avoid blocking the event loop.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
(async () => {
|
||||
try {
|
||||
const userAccountManager = new UserAccountManager();
|
||||
const email = userAccountManager.getCachedGoogleAccount();
|
||||
const tierName = config.getUserTierName();
|
||||
|
||||
if (authType) {
|
||||
let message =
|
||||
authType === AuthType.LOGIN_WITH_GOOGLE
|
||||
? email
|
||||
? `Logged in with Google: ${email}`
|
||||
: 'Logged in with Google'
|
||||
: `Authenticated with ${authType}`;
|
||||
if (tierName) {
|
||||
message += ` (Plan: ${tierName})`;
|
||||
}
|
||||
addItem({
|
||||
type: MessageType.INFO,
|
||||
text: message,
|
||||
});
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore errors during initial auth check
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
config,
|
||||
resumedSessionData,
|
||||
settings.merged.ui.showUserIdentity,
|
||||
addItem,
|
||||
]);
|
||||
|
||||
useMemoryMonitor(historyManager);
|
||||
const settings = useSettings();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const [corgiMode, setCorgiMode] = useState(false);
|
||||
const [debugMessage, setDebugMessage] = useState<string>('');
|
||||
@@ -209,6 +255,10 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
);
|
||||
const [copyModeEnabled, setCopyModeEnabled] = useState(false);
|
||||
const [pendingRestorePrompt, setPendingRestorePrompt] = useState(false);
|
||||
const toggleBackgroundShellRef = useRef<() => void>(() => {});
|
||||
const isBackgroundShellVisibleRef = useRef<boolean>(false);
|
||||
const backgroundShellsRef = useRef<Map<number, BackgroundShell>>(new Map());
|
||||
|
||||
const [adminSettingsChanged, setAdminSettingsChanged] = useState(false);
|
||||
|
||||
const [shellModeActive, setShellModeActive] = useState(false);
|
||||
@@ -288,11 +338,6 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
AgentDefinition | undefined
|
||||
>();
|
||||
|
||||
// AskUser dialog state
|
||||
const [askUserRequest, setAskUserRequest] = useState<AskUserState | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const openAgentConfigDialog = useCallback(
|
||||
(name: string, displayName: string, definition: AgentDefinition) => {
|
||||
setSelectedAgentName(name);
|
||||
@@ -310,56 +355,6 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
setSelectedAgentDefinition(undefined);
|
||||
}, []);
|
||||
|
||||
// Subscribe to ASK_USER_REQUEST messages from the message bus
|
||||
useEffect(() => {
|
||||
const messageBus = config.getMessageBus();
|
||||
|
||||
const handler = (msg: AskUserRequest) => {
|
||||
setAskUserRequest({
|
||||
questions: msg.questions,
|
||||
correlationId: msg.correlationId,
|
||||
});
|
||||
};
|
||||
|
||||
messageBus.subscribe(MessageBusType.ASK_USER_REQUEST, handler);
|
||||
|
||||
return () => {
|
||||
messageBus.unsubscribe(MessageBusType.ASK_USER_REQUEST, handler);
|
||||
};
|
||||
}, [config]);
|
||||
|
||||
// Handler to submit ask_user answers
|
||||
const handleAskUserSubmit = useCallback(
|
||||
async (answers: { [questionIndex: string]: string }) => {
|
||||
if (!askUserRequest) return;
|
||||
|
||||
const messageBus = config.getMessageBus();
|
||||
await messageBus.publish({
|
||||
type: MessageBusType.ASK_USER_RESPONSE,
|
||||
correlationId: askUserRequest.correlationId,
|
||||
answers,
|
||||
});
|
||||
|
||||
setAskUserRequest(null);
|
||||
},
|
||||
[config, askUserRequest],
|
||||
);
|
||||
|
||||
// Handler to cancel ask_user dialog
|
||||
const handleAskUserCancel = useCallback(async () => {
|
||||
if (!askUserRequest) return;
|
||||
|
||||
const messageBus = config.getMessageBus();
|
||||
await messageBus.publish({
|
||||
type: MessageBusType.ASK_USER_RESPONSE,
|
||||
correlationId: askUserRequest.correlationId,
|
||||
answers: {},
|
||||
cancelled: true,
|
||||
});
|
||||
|
||||
setAskUserRequest(null);
|
||||
}, [config, askUserRequest]);
|
||||
|
||||
const toggleDebugProfiler = useCallback(
|
||||
() => setShowDebugProfiler((prev) => !prev),
|
||||
[],
|
||||
@@ -439,6 +434,12 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
registerCleanup(async () => {
|
||||
// Turn off mouse scroll.
|
||||
disableMouseEvents();
|
||||
|
||||
// Kill all background shells
|
||||
for (const pid of backgroundShellsRef.current.keys()) {
|
||||
ShellExecutionService.kill(pid);
|
||||
}
|
||||
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
await ideClient.disconnect();
|
||||
|
||||
@@ -531,6 +532,14 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
shellModeActive,
|
||||
getPreferredEditor,
|
||||
});
|
||||
const bufferRef = useRef(buffer);
|
||||
useEffect(() => {
|
||||
bufferRef.current = buffer;
|
||||
}, [buffer]);
|
||||
|
||||
const stableSetText = useCallback((text: string) => {
|
||||
bufferRef.current.setText(text);
|
||||
}, []);
|
||||
|
||||
// Initialize input history from logger (past sessions)
|
||||
useEffect(() => {
|
||||
@@ -787,6 +796,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
const { toggleVimEnabled } = useVimMode();
|
||||
|
||||
const setIsBackgroundShellListOpenRef = useRef<(open: boolean) => void>(
|
||||
() => {},
|
||||
);
|
||||
|
||||
const slashCommandActions = useMemo(
|
||||
() => ({
|
||||
openAuthDialog: () => setAuthState(AuthState.Updating),
|
||||
@@ -810,7 +823,18 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
toggleDebugProfiler,
|
||||
dispatchExtensionStateUpdate,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
setText: (text: string) => buffer.setText(text),
|
||||
toggleBackgroundShell: () => {
|
||||
toggleBackgroundShellRef.current();
|
||||
if (!isBackgroundShellVisibleRef.current) {
|
||||
setEmbeddedShellFocused(true);
|
||||
if (backgroundShellsRef.current.size > 1) {
|
||||
setIsBackgroundShellListOpenRef.current(true);
|
||||
} else {
|
||||
setIsBackgroundShellListOpenRef.current(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
setText: stableSetText,
|
||||
}),
|
||||
[
|
||||
setAuthState,
|
||||
@@ -828,7 +852,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
openPermissionsDialog,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
toggleDebugProfiler,
|
||||
buffer,
|
||||
stableSetText,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -837,7 +861,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
slashCommands,
|
||||
pendingHistoryItems: pendingSlashCommandHistoryItems,
|
||||
commandContext,
|
||||
confirmationRequest,
|
||||
confirmationRequest: commandConfirmationRequest,
|
||||
} = useSlashCommandProcessor(
|
||||
config,
|
||||
settings,
|
||||
@@ -854,6 +878,26 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
setCustomDialog,
|
||||
);
|
||||
|
||||
const [authConsentRequest, setAuthConsentRequest] =
|
||||
useState<ConfirmationRequest | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleConsentRequest = (payload: ConsentRequestPayload) => {
|
||||
setAuthConsentRequest({
|
||||
prompt: payload.prompt,
|
||||
onConfirm: (confirmed: boolean) => {
|
||||
setAuthConsentRequest(null);
|
||||
payload.onConfirm(confirmed);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
coreEvents.on(CoreEvent.ConsentRequest, handleConsentRequest);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.ConsentRequest, handleConsentRequest);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const performMemoryRefresh = useCallback(async () => {
|
||||
historyManager.addItem(
|
||||
{
|
||||
@@ -941,6 +985,12 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
lastOutputTime,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
toggleBackgroundShell,
|
||||
backgroundCurrentShell,
|
||||
backgroundShells,
|
||||
dismissBackgroundShell,
|
||||
retryStatus,
|
||||
} = useGeminiStream(
|
||||
config.getGeminiClient(),
|
||||
@@ -963,7 +1013,30 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
embeddedShellFocused,
|
||||
);
|
||||
|
||||
toggleBackgroundShellRef.current = toggleBackgroundShell;
|
||||
isBackgroundShellVisibleRef.current = isBackgroundShellVisible;
|
||||
backgroundShellsRef.current = backgroundShells;
|
||||
|
||||
const {
|
||||
activeBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
isBackgroundShellListOpen,
|
||||
setActiveBackgroundShellPid,
|
||||
backgroundShellHeight,
|
||||
} = useBackgroundShellManager({
|
||||
backgroundShells,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
setEmbeddedShellFocused,
|
||||
terminalHeight,
|
||||
});
|
||||
|
||||
setIsBackgroundShellListOpenRef.current = setIsBackgroundShellListOpen;
|
||||
|
||||
const lastOutputTimeRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
lastOutputTimeRef.current = lastOutputTime;
|
||||
}, [lastOutputTime]);
|
||||
@@ -1112,7 +1185,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
// Compute available terminal height based on controls measurement
|
||||
const availableTerminalHeight = Math.max(
|
||||
0,
|
||||
terminalHeight - controlsHeight - staticExtraHeight - 2,
|
||||
terminalHeight -
|
||||
controlsHeight -
|
||||
staticExtraHeight -
|
||||
2 -
|
||||
backgroundShellHeight,
|
||||
);
|
||||
|
||||
config.setShellExecutionConfig({
|
||||
@@ -1336,7 +1413,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
if (ctrlCPressCount > 1) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
handleSlashCommand('/quit', undefined, undefined, false);
|
||||
} else {
|
||||
} else if (ctrlCPressCount > 0) {
|
||||
ctrlCTimerRef.current = setTimeout(() => {
|
||||
setCtrlCPressCount(0);
|
||||
ctrlCTimerRef.current = null;
|
||||
@@ -1355,7 +1432,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
if (ctrlDPressCount > 1) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
handleSlashCommand('/quit', undefined, undefined, false);
|
||||
} else {
|
||||
} else if (ctrlDPressCount > 0) {
|
||||
ctrlDTimerRef.current = setTimeout(() => {
|
||||
setCtrlDPressCount(0);
|
||||
ctrlDTimerRef.current = null;
|
||||
@@ -1396,7 +1473,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
});
|
||||
|
||||
const handleGlobalKeypress = useCallback(
|
||||
(key: Key) => {
|
||||
(key: Key): boolean => {
|
||||
if (copyModeEnabled) {
|
||||
setCopyModeEnabled(false);
|
||||
enableMouseEvents();
|
||||
@@ -1416,10 +1493,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.QUIT](key)) {
|
||||
// Skip when ask_user dialog is open (use Esc to cancel instead)
|
||||
if (askUserRequest) {
|
||||
return;
|
||||
}
|
||||
// If the user presses Ctrl+C, we want to cancel any ongoing requests.
|
||||
// This should happen regardless of the count.
|
||||
cancelOngoingRequest?.();
|
||||
@@ -1427,9 +1500,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
setCtrlCPressCount((prev) => prev + 1);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.EXIT](key)) {
|
||||
if (buffer.text.length > 0) {
|
||||
return false;
|
||||
}
|
||||
setCtrlDPressCount((prev) => prev + 1);
|
||||
return true;
|
||||
}
|
||||
@@ -1443,6 +1513,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
|
||||
setShowErrorDetails((prev) => !prev);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SUSPEND_APP](key)) {
|
||||
handleWarning('Undo has been moved to Cmd + Z or Alt/Opt + Z');
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
|
||||
setShowFullTodos((prev) => !prev);
|
||||
return true;
|
||||
@@ -1469,9 +1542,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
setConstrainHeight(false);
|
||||
return true;
|
||||
} else if (
|
||||
keyMatchers[Command.UNFOCUS_SHELL_INPUT](key) &&
|
||||
activePtyId &&
|
||||
embeddedShellFocused
|
||||
keyMatchers[Command.FOCUS_SHELL_INPUT](key) &&
|
||||
(activePtyId || (isBackgroundShellVisible && backgroundShells.size > 0))
|
||||
) {
|
||||
if (key.name === 'tab' && key.shift) {
|
||||
// Always change focus
|
||||
@@ -1479,26 +1551,72 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (embeddedShellFocused) {
|
||||
handleWarning('Press Shift+Tab to focus out.');
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
// If the shell hasn't produced output in the last 100ms, it's considered idle.
|
||||
const isIdle = now - lastOutputTimeRef.current >= 100;
|
||||
if (isIdle) {
|
||||
if (isIdle && !activePtyId) {
|
||||
if (tabFocusTimeoutRef.current) {
|
||||
clearTimeout(tabFocusTimeoutRef.current);
|
||||
}
|
||||
tabFocusTimeoutRef.current = setTimeout(() => {
|
||||
tabFocusTimeoutRef.current = null;
|
||||
// If the shell produced output since the tab press, we assume it handled the tab
|
||||
// (e.g. autocomplete) so we should not toggle focus.
|
||||
if (lastOutputTimeRef.current > now) {
|
||||
handleWarning('Press Shift+Tab to focus out.');
|
||||
return;
|
||||
toggleBackgroundShell();
|
||||
if (!isBackgroundShellVisible) {
|
||||
// We are about to show it, so focus it
|
||||
setEmbeddedShellFocused(true);
|
||||
if (backgroundShells.size > 1) {
|
||||
setIsBackgroundShellListOpen(true);
|
||||
}
|
||||
setEmbeddedShellFocused(false);
|
||||
}, 100);
|
||||
} else {
|
||||
// We are about to hide it
|
||||
tabFocusTimeoutRef.current = setTimeout(() => {
|
||||
tabFocusTimeoutRef.current = null;
|
||||
// If the shell produced output since the tab press, we assume it handled the tab
|
||||
// (e.g. autocomplete) so we should not toggle focus.
|
||||
if (lastOutputTimeRef.current > now) {
|
||||
handleWarning('Press Shift+Tab to focus out.');
|
||||
return;
|
||||
}
|
||||
setEmbeddedShellFocused(false);
|
||||
}, 100);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
handleWarning('Press Shift+Tab to focus out.');
|
||||
|
||||
// Not idle, just focus it
|
||||
setEmbeddedShellFocused(true);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL](key)) {
|
||||
if (activePtyId) {
|
||||
backgroundCurrentShell();
|
||||
// After backgrounding, we explicitly do NOT show or focus the background UI.
|
||||
} else {
|
||||
if (isBackgroundShellVisible && !embeddedShellFocused) {
|
||||
setEmbeddedShellFocused(true);
|
||||
} else {
|
||||
toggleBackgroundShell();
|
||||
// Toggle focus based on intent: if we were hiding, unfocus; if showing, focus.
|
||||
if (!isBackgroundShellVisible && backgroundShells.size > 0) {
|
||||
setEmbeddedShellFocused(true);
|
||||
if (backgroundShells.size > 1) {
|
||||
setIsBackgroundShellListOpen(true);
|
||||
}
|
||||
} else {
|
||||
setEmbeddedShellFocused(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL_LIST](key)) {
|
||||
if (backgroundShells.size > 0 && isBackgroundShellVisible) {
|
||||
if (!embeddedShellFocused) {
|
||||
setEmbeddedShellFocused(true);
|
||||
}
|
||||
setIsBackgroundShellListOpen(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -1510,11 +1628,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
config,
|
||||
ideContextState,
|
||||
setCtrlCPressCount,
|
||||
buffer.text.length,
|
||||
setCtrlDPressCount,
|
||||
handleSlashCommand,
|
||||
cancelOngoingRequest,
|
||||
askUserRequest,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
settings.merged.general.debugKeystrokeLogging,
|
||||
@@ -1522,6 +1638,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
setCopyModeEnabled,
|
||||
copyModeEnabled,
|
||||
isAlternateBuffer,
|
||||
backgroundCurrentShell,
|
||||
toggleBackgroundShell,
|
||||
backgroundShells,
|
||||
isBackgroundShellVisible,
|
||||
setIsBackgroundShellListOpen,
|
||||
lastOutputTimeRef,
|
||||
tabFocusTimeoutRef,
|
||||
handleWarning,
|
||||
],
|
||||
);
|
||||
@@ -1535,7 +1658,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
const paddedTitle = computeTerminalTitle({
|
||||
streamingState,
|
||||
thoughtSubject: thought?.subject,
|
||||
isConfirming: !!confirmationRequest || shouldShowActionRequiredTitle,
|
||||
isConfirming:
|
||||
!!commandConfirmationRequest || shouldShowActionRequiredTitle,
|
||||
isSilentWorking: shouldShowSilentWorkingTitle,
|
||||
folderName: basename(config.getTargetDir()),
|
||||
showThoughts: !!settings.merged.ui.showStatusInTitle,
|
||||
@@ -1551,7 +1675,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}, [
|
||||
streamingState,
|
||||
thought,
|
||||
confirmationRequest,
|
||||
commandConfirmationRequest,
|
||||
shouldShowActionRequiredTitle,
|
||||
shouldShowSilentWorkingTitle,
|
||||
settings.merged.ui.showStatusInTitle,
|
||||
@@ -1627,11 +1751,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
const nightly = props.version.includes('nightly');
|
||||
|
||||
const dialogsVisible =
|
||||
!!askUserRequest ||
|
||||
shouldShowIdePrompt ||
|
||||
isFolderTrustDialogOpen ||
|
||||
adminSettingsChanged ||
|
||||
!!confirmationRequest ||
|
||||
!!commandConfirmationRequest ||
|
||||
!!authConsentRequest ||
|
||||
!!customDialog ||
|
||||
confirmUpdateExtensionRequests.length > 0 ||
|
||||
!!loopDetectionConfirmationRequest ||
|
||||
@@ -1741,7 +1865,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
slashCommands,
|
||||
pendingSlashCommandHistoryItems,
|
||||
commandContext,
|
||||
confirmationRequest,
|
||||
commandConfirmationRequest,
|
||||
authConsentRequest,
|
||||
confirmUpdateExtensionRequests,
|
||||
loopDetectionConfirmationRequest,
|
||||
geminiMdFileCount,
|
||||
@@ -1802,6 +1927,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isRestarting,
|
||||
extensionsUpdateState,
|
||||
activePtyId,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
embeddedShellFocused,
|
||||
showDebugProfiler,
|
||||
customDialog,
|
||||
@@ -1811,6 +1938,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
bannerVisible,
|
||||
terminalBackgroundColor: config.getTerminalBackground(),
|
||||
settingsNonce,
|
||||
backgroundShells,
|
||||
activeBackgroundShellPid,
|
||||
backgroundShellHeight,
|
||||
isBackgroundShellListOpen,
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
}),
|
||||
@@ -1839,7 +1970,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
slashCommands,
|
||||
pendingSlashCommandHistoryItems,
|
||||
commandContext,
|
||||
confirmationRequest,
|
||||
commandConfirmationRequest,
|
||||
authConsentRequest,
|
||||
confirmUpdateExtensionRequests,
|
||||
loopDetectionConfirmationRequest,
|
||||
geminiMdFileCount,
|
||||
@@ -1900,6 +2032,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
currentModel,
|
||||
extensionsUpdateState,
|
||||
activePtyId,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
historyManager,
|
||||
embeddedShellFocused,
|
||||
showDebugProfiler,
|
||||
@@ -1912,6 +2046,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
bannerVisible,
|
||||
config,
|
||||
settingsNonce,
|
||||
backgroundShellHeight,
|
||||
isBackgroundShellListOpen,
|
||||
activeBackgroundShellPid,
|
||||
backgroundShells,
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
],
|
||||
@@ -1959,7 +2097,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
handleApiKeySubmit,
|
||||
handleApiKeyCancel,
|
||||
setBannerVisible,
|
||||
handleWarning,
|
||||
setEmbeddedShellFocused,
|
||||
dismissBackgroundShell,
|
||||
setActiveBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
setAuthContext,
|
||||
handleRestart: async () => {
|
||||
if (process.send) {
|
||||
@@ -2031,7 +2173,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
handleApiKeySubmit,
|
||||
handleApiKeyCancel,
|
||||
setBannerVisible,
|
||||
handleWarning,
|
||||
setEmbeddedShellFocused,
|
||||
dismissBackgroundShell,
|
||||
setActiveBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
setAuthContext,
|
||||
newAgents,
|
||||
config,
|
||||
@@ -2062,15 +2208,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}}
|
||||
>
|
||||
<ToolActionsProvider config={config} toolCalls={allToolCalls}>
|
||||
<AskUserActionsProvider
|
||||
request={askUserRequest}
|
||||
onSubmit={handleAskUserSubmit}
|
||||
onCancel={handleAskUserCancel}
|
||||
>
|
||||
<ShellFocusContext.Provider value={isFocused}>
|
||||
<App />
|
||||
</ShellFocusContext.Provider>
|
||||
</AskUserActionsProvider>
|
||||
<ShellFocusContext.Provider value={isFocused}>
|
||||
<App />
|
||||
</ShellFocusContext.Provider>
|
||||
</ToolActionsProvider>
|
||||
</AppContext.Provider>
|
||||
</ConfigContext.Provider>
|
||||
|
||||
@@ -125,7 +125,7 @@ Tips for getting started:
|
||||
4. /help for more information.
|
||||
HistoryItemDisplay
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Action Required 1 of 1 │
|
||||
│ Action Required │
|
||||
│ │
|
||||
│ ? ls list directory │
|
||||
│ │
|
||||
|
||||
@@ -92,6 +92,7 @@ describe('bugCommand', () => {
|
||||
getHistory: () => [],
|
||||
}),
|
||||
}),
|
||||
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -106,6 +107,7 @@ describe('bugCommand', () => {
|
||||
* **Operating System:** test-platform v20.0.0
|
||||
* **Sandbox Environment:** test
|
||||
* **Model Version:** gemini-pro
|
||||
* **Auth Type:** oauth-personal
|
||||
* **Memory Usage:** 100 MB
|
||||
* **Terminal Name:** Test Terminal
|
||||
* **Terminal Background:** #000000
|
||||
@@ -133,6 +135,7 @@ describe('bugCommand', () => {
|
||||
getHistory: () => history,
|
||||
}),
|
||||
}),
|
||||
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
|
||||
storage: {
|
||||
getProjectTempDir: () => '/tmp/gemini',
|
||||
},
|
||||
@@ -178,6 +181,7 @@ describe('bugCommand', () => {
|
||||
getHistory: () => [],
|
||||
}),
|
||||
}),
|
||||
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -192,6 +196,7 @@ describe('bugCommand', () => {
|
||||
* **Operating System:** test-platform v20.0.0
|
||||
* **Sandbox Environment:** test
|
||||
* **Model Version:** gemini-pro
|
||||
* **Auth Type:** vertex-ai
|
||||
* **Memory Usage:** 100 MB
|
||||
* **Terminal Name:** Test Terminal
|
||||
* **Terminal Background:** #000000
|
||||
|
||||
@@ -54,6 +54,7 @@ export const bugCommand: SlashCommand = {
|
||||
const kittyProtocol = terminalCapabilityManager.isKittyProtocolEnabled()
|
||||
? 'Supported'
|
||||
: 'Unsupported';
|
||||
const authType = config?.getContentGeneratorConfig()?.authType || 'Unknown';
|
||||
|
||||
let info = `
|
||||
* **CLI Version:** ${cliVersion}
|
||||
@@ -62,6 +63,7 @@ export const bugCommand: SlashCommand = {
|
||||
* **Operating System:** ${osVersion}
|
||||
* **Sandbox Environment:** ${sandboxEnv}
|
||||
* **Model Version:** ${modelVersion}
|
||||
* **Auth Type:** ${authType}
|
||||
* **Memory Usage:** ${memoryUsage}
|
||||
* **Terminal Name:** ${terminalName}
|
||||
* **Terminal Background:** ${terminalBgColor}
|
||||
|
||||
@@ -67,6 +67,9 @@ describe('directoryCommand', () => {
|
||||
isRestrictiveSandbox: vi.fn().mockReturnValue(false),
|
||||
getGeminiClient: vi.fn().mockReturnValue({
|
||||
addDirectoryContext: vi.fn(),
|
||||
getChatRecordingService: vi.fn().mockReturnValue({
|
||||
recordDirectories: vi.fn(),
|
||||
}),
|
||||
}),
|
||||
getWorkingDir: () => path.resolve('/test/dir'),
|
||||
shouldLoadMemoryFromIncludeDirectories: () => false,
|
||||
|
||||
@@ -57,6 +57,13 @@ async function finishAddingDirectories(
|
||||
const gemini = config.getGeminiClient();
|
||||
if (gemini) {
|
||||
await gemini.addDirectoryContext();
|
||||
|
||||
// Persist directories to session file for resume support
|
||||
const chatRecordingService = gemini.getChatRecordingService();
|
||||
const workspaceContext = config.getWorkspaceContext();
|
||||
chatRecordingService?.recordDirectories(
|
||||
workspaceContext.getDirectories(),
|
||||
);
|
||||
}
|
||||
addItem({
|
||||
type: MessageType.INFO,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MessageType } from '../types.js';
|
||||
import type { HookRegistryEntry } from '@google/gemini-cli-core';
|
||||
import { HookType, HookEventName, ConfigSource } from '@google/gemini-cli-core';
|
||||
import type { CommandContext } from './types.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
|
||||
describe('hooksCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
@@ -29,11 +30,13 @@ describe('hooksCommand', () => {
|
||||
hooksConfig?: {
|
||||
disabled?: string[];
|
||||
};
|
||||
tools?: {
|
||||
enableHooks?: boolean;
|
||||
};
|
||||
};
|
||||
setValue: ReturnType<typeof vi.fn>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
workspace: { path: string; settings: any };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
user: { path: string; settings: any };
|
||||
forScope: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -56,6 +59,17 @@ describe('hooksCommand', () => {
|
||||
};
|
||||
|
||||
// Create mock settings
|
||||
const mockUser = {
|
||||
path: '/mock/user.json',
|
||||
settings: { hooksConfig: { disabled: [] } },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
const mockWorkspace = {
|
||||
path: '/mock/workspace.json',
|
||||
settings: { hooksConfig: { disabled: [] } },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
|
||||
mockSettings = {
|
||||
merged: {
|
||||
hooksConfig: {
|
||||
@@ -63,7 +77,15 @@ describe('hooksCommand', () => {
|
||||
},
|
||||
},
|
||||
setValue: vi.fn(),
|
||||
};
|
||||
workspace: mockWorkspace,
|
||||
user: mockUser,
|
||||
forScope: vi.fn((scope) => {
|
||||
if (scope === SettingScope.User) return mockUser;
|
||||
if (scope === SettingScope.Workspace) return mockWorkspace;
|
||||
return mockUser;
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
|
||||
// Create mock context with config and settings
|
||||
mockContext = createMockCommandContext({
|
||||
@@ -162,8 +184,8 @@ describe('hooksCommand', () => {
|
||||
it('should display panel when no hooks are configured', async () => {
|
||||
mockHookSystem.getAllHooks.mockReturnValue([]);
|
||||
(mockContext.services.settings.merged as Record<string, unknown>)[
|
||||
'tools'
|
||||
] = { enableHooks: true };
|
||||
'hooksConfig'
|
||||
] = { enabled: true };
|
||||
|
||||
const panelCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'panel',
|
||||
@@ -190,8 +212,8 @@ describe('hooksCommand', () => {
|
||||
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
(mockContext.services.settings.merged as Record<string, unknown>)[
|
||||
'tools'
|
||||
] = { enableHooks: true };
|
||||
'hooksConfig'
|
||||
] = { enabled: true };
|
||||
|
||||
const panelCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'panel',
|
||||
@@ -272,11 +294,12 @@ describe('hooksCommand', () => {
|
||||
});
|
||||
|
||||
it('should enable a hook and update settings', async () => {
|
||||
// Update the context's settings with disabled hooks
|
||||
mockContext.services.settings.merged.hooksConfig.disabled = [
|
||||
// Update the user settings with disabled hooks
|
||||
mockSettings.user.settings.hooksConfig.disabled = [
|
||||
'test-hook',
|
||||
'other-hook',
|
||||
];
|
||||
mockSettings.workspace.settings.hooksConfig.disabled = [];
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
@@ -288,7 +311,7 @@ describe('hooksCommand', () => {
|
||||
const result = await enableCmd.action(mockContext, 'test-hook');
|
||||
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
SettingScope.User,
|
||||
'hooksConfig.disabled',
|
||||
['other-hook'],
|
||||
);
|
||||
@@ -299,28 +322,8 @@ describe('hooksCommand', () => {
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Hook "test-hook" enabled successfully.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle error when enabling hook fails', async () => {
|
||||
mockSettings.setValue.mockImplementationOnce(() => {
|
||||
throw new Error('Failed to save settings');
|
||||
});
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
);
|
||||
if (!enableCmd?.action) {
|
||||
throw new Error('enable command must have an action');
|
||||
}
|
||||
|
||||
const result = await enableCmd.action(mockContext, 'test-hook');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Failed to enable hook: Failed to save settings',
|
||||
content:
|
||||
'Hook "test-hook" enabled by removing it from the disabled list in user (/mock/user.json) and workspace (/mock/workspace.json) settings.',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -332,7 +335,7 @@ describe('hooksCommand', () => {
|
||||
const hookEntry = createMockHook(
|
||||
'./hooks/test.sh',
|
||||
HookEventName.BeforeTool,
|
||||
true,
|
||||
false, // Must be disabled for enable completion
|
||||
);
|
||||
hookEntry.config.name = 'friendly-name';
|
||||
|
||||
@@ -404,7 +407,9 @@ describe('hooksCommand', () => {
|
||||
});
|
||||
|
||||
it('should disable a hook and update settings', async () => {
|
||||
mockContext.services.settings.merged.hooksConfig.disabled = [];
|
||||
// Ensure not disabled anywhere
|
||||
mockSettings.workspace.settings.hooksConfig.disabled = [];
|
||||
mockSettings.user.settings.hooksConfig.disabled = [];
|
||||
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
@@ -415,8 +420,9 @@ describe('hooksCommand', () => {
|
||||
|
||||
const result = await disableCmd.action(mockContext, 'test-hook');
|
||||
|
||||
// Should default to workspace if present
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
SettingScope.Workspace,
|
||||
'hooksConfig.disabled',
|
||||
['test-hook'],
|
||||
);
|
||||
@@ -427,13 +433,14 @@ describe('hooksCommand', () => {
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Hook "test-hook" disabled successfully.',
|
||||
content:
|
||||
'Hook "test-hook" disabled by adding it to the disabled list in workspace (/mock/workspace.json) settings.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should synchronize with hook system even if hook is already in disabled list', async () => {
|
||||
// Update the context's settings with the hook already disabled
|
||||
mockContext.services.settings.merged.hooksConfig.disabled = ['test-hook'];
|
||||
it('should return info when hook is already disabled', async () => {
|
||||
// Update the context's settings with the hook already disabled in Workspace
|
||||
mockSettings.workspace.settings.hooksConfig.disabled = ['test-hook'];
|
||||
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
@@ -445,38 +452,29 @@ describe('hooksCommand', () => {
|
||||
const result = await disableCmd.action(mockContext, 'test-hook');
|
||||
|
||||
expect(mockContext.services.settings.setValue).not.toHaveBeenCalled();
|
||||
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
|
||||
'test-hook',
|
||||
false,
|
||||
);
|
||||
expect(mockConfig.updateDisabledHooks).toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Hook "test-hook" disabled successfully.',
|
||||
content: 'Hook "test-hook" is already disabled.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle error when disabling hook fails', async () => {
|
||||
mockContext.services.settings.merged.hooksConfig.disabled = [];
|
||||
mockSettings.setValue.mockImplementationOnce(() => {
|
||||
throw new Error('Failed to save settings');
|
||||
});
|
||||
|
||||
it('should complete hook names using friendly names', () => {
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
)!;
|
||||
|
||||
const hookEntry = createMockHook(
|
||||
'./hooks/test.sh',
|
||||
HookEventName.BeforeTool,
|
||||
true, // Must be enabled for disable completion
|
||||
);
|
||||
if (!disableCmd?.action) {
|
||||
throw new Error('disable command must have an action');
|
||||
}
|
||||
hookEntry.config.name = 'friendly-name';
|
||||
|
||||
const result = await disableCmd.action(mockContext, 'test-hook');
|
||||
mockHookSystem.getAllHooks.mockReturnValue([hookEntry]);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Failed to disable hook: Failed to save settings',
|
||||
});
|
||||
const completions = disableCmd.completion!(mockContext, 'frie');
|
||||
expect(completions).toContain('friendly-name');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -513,50 +511,52 @@ describe('hooksCommand', () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return matching hook names', () => {
|
||||
it('should return matching hook names based on status', () => {
|
||||
const mockHooks: HookRegistryEntry[] = [
|
||||
createMockHook('test-hook-1', HookEventName.BeforeTool, true),
|
||||
createMockHook('test-hook-2', HookEventName.AfterTool, true),
|
||||
createMockHook('other-hook', HookEventName.AfterAgent, false),
|
||||
createMockHook('test-hook-enabled', HookEventName.BeforeTool, true),
|
||||
createMockHook('test-hook-disabled', HookEventName.AfterTool, false),
|
||||
];
|
||||
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
);
|
||||
if (!enableCmd?.completion) {
|
||||
throw new Error('enable command must have completion');
|
||||
}
|
||||
)!;
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
)!;
|
||||
|
||||
const result = enableCmd.completion(mockContext, 'test');
|
||||
expect(result).toEqual(['test-hook-1', 'test-hook-2']);
|
||||
const enableResult = enableCmd.completion!(mockContext, 'test');
|
||||
expect(enableResult).toEqual(['test-hook-disabled']);
|
||||
|
||||
const disableResult = disableCmd.completion!(mockContext, 'test');
|
||||
expect(disableResult).toEqual(['test-hook-enabled']);
|
||||
});
|
||||
|
||||
it('should return all hook names when partial is empty', () => {
|
||||
it('should return all relevant hook names when partial is empty', () => {
|
||||
const mockHooks: HookRegistryEntry[] = [
|
||||
createMockHook('hook-1', HookEventName.BeforeTool, true),
|
||||
createMockHook('hook-2', HookEventName.AfterTool, true),
|
||||
createMockHook('hook-enabled', HookEventName.BeforeTool, true),
|
||||
createMockHook('hook-disabled', HookEventName.AfterTool, false),
|
||||
];
|
||||
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
);
|
||||
if (!enableCmd?.completion) {
|
||||
throw new Error('enable command must have completion');
|
||||
}
|
||||
)!;
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
)!;
|
||||
|
||||
const result = enableCmd.completion(mockContext, '');
|
||||
expect(result).toEqual(['hook-1', 'hook-2']);
|
||||
expect(enableCmd.completion!(mockContext, '')).toEqual(['hook-disabled']);
|
||||
expect(disableCmd.completion!(mockContext, '')).toEqual(['hook-enabled']);
|
||||
});
|
||||
|
||||
it('should handle hooks without command name gracefully', () => {
|
||||
const mockHooks: HookRegistryEntry[] = [
|
||||
createMockHook('test-hook', HookEventName.BeforeTool, true),
|
||||
createMockHook('test-hook', HookEventName.BeforeTool, false),
|
||||
{
|
||||
...createMockHook('', HookEventName.AfterTool, true),
|
||||
...createMockHook('', HookEventName.AfterTool, false),
|
||||
config: { command: '', type: HookType.Command, timeout: 30 },
|
||||
},
|
||||
];
|
||||
@@ -636,7 +636,7 @@ describe('hooksCommand', () => {
|
||||
const result = await enableAllCmd.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(String), // enableAll uses legacy logic so it might return 'Workspace' or 'User' depending on ternary
|
||||
'hooksConfig.disabled',
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -12,7 +12,9 @@ import type {
|
||||
MessageActionReturn,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { SettingScope, isLoadableSettingScope } from '../../config/settings.js';
|
||||
import { enableHook, disableHook } from '../../utils/hookSettings.js';
|
||||
import { renderHookActionFeedback } from '../../utils/hookUtils.js';
|
||||
|
||||
/**
|
||||
* Display a formatted list of hooks with their status
|
||||
@@ -74,39 +76,23 @@ async function enableAction(
|
||||
};
|
||||
}
|
||||
|
||||
// Get current disabled hooks from settings
|
||||
const settings = context.services.settings;
|
||||
const disabledHooks = settings.merged.hooksConfig.disabled;
|
||||
// Remove from disabled list if present
|
||||
const newDisabledHooks = disabledHooks.filter(
|
||||
(name: string) => name !== hookName,
|
||||
const result = enableHook(settings, hookName);
|
||||
|
||||
if (result.status === 'success') {
|
||||
hookSystem.setHookEnabled(hookName, true);
|
||||
}
|
||||
|
||||
const feedback = renderHookActionFeedback(
|
||||
result,
|
||||
(label, path) => `${label} (${path})`,
|
||||
);
|
||||
|
||||
// Update settings (setValue automatically saves)
|
||||
try {
|
||||
const scope = settings.workspace
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooksConfig.disabled', newDisabledHooks);
|
||||
|
||||
// Update core config so re-initialization (e.g. extension reload) respects the change
|
||||
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
||||
|
||||
// Enable in hook system
|
||||
hookSystem.setHookEnabled(hookName, true);
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Hook "${hookName}" enabled successfully.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to enable hook: ${getErrorMessage(error)}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: result.status === 'error' ? 'error' : 'info',
|
||||
content: feedback,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,44 +129,31 @@ async function disableAction(
|
||||
};
|
||||
}
|
||||
|
||||
// Get current disabled hooks from settings
|
||||
const settings = context.services.settings;
|
||||
const disabledHooks = settings.merged.hooksConfig.disabled;
|
||||
// Add to disabled list if not already present
|
||||
try {
|
||||
if (!disabledHooks.includes(hookName)) {
|
||||
const newDisabledHooks = [...disabledHooks, hookName];
|
||||
const scope = settings.workspace ? SettingScope.Workspace : SettingScope.User;
|
||||
|
||||
const scope = settings.workspace
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooksConfig.disabled', newDisabledHooks);
|
||||
}
|
||||
const result = disableHook(settings, hookName, scope);
|
||||
|
||||
// Update core config so re-initialization (e.g. extension reload) respects the change
|
||||
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
||||
|
||||
// Always disable in hook system to ensure in-memory state matches settings
|
||||
if (result.status === 'success') {
|
||||
hookSystem.setHookEnabled(hookName, false);
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Hook "${hookName}" disabled successfully.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to disable hook: ${getErrorMessage(error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
const feedback = renderHookActionFeedback(
|
||||
result,
|
||||
(label, path) => `${label} (${path})`,
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: result.status === 'error' ? 'error' : 'info',
|
||||
content: feedback,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Completion function for hook names
|
||||
* Completion function for enabled hook names (to be disabled)
|
||||
*/
|
||||
function completeHookNames(
|
||||
function completeEnabledHookNames(
|
||||
context: CommandContext,
|
||||
partialArg: string,
|
||||
): string[] {
|
||||
@@ -191,8 +164,30 @@ function completeHookNames(
|
||||
if (!hookSystem) return [];
|
||||
|
||||
const allHooks = hookSystem.getAllHooks();
|
||||
const hookNames = allHooks.map((hook) => getHookDisplayName(hook));
|
||||
return hookNames.filter((name) => name.startsWith(partialArg));
|
||||
return allHooks
|
||||
.filter((hook) => hook.enabled)
|
||||
.map((hook) => getHookDisplayName(hook))
|
||||
.filter((name) => name.startsWith(partialArg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Completion function for disabled hook names (to be enabled)
|
||||
*/
|
||||
function completeDisabledHookNames(
|
||||
context: CommandContext,
|
||||
partialArg: string,
|
||||
): string[] {
|
||||
const { config } = context.services;
|
||||
if (!config) return [];
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
if (!hookSystem) return [];
|
||||
|
||||
const allHooks = hookSystem.getAllHooks();
|
||||
return allHooks
|
||||
.filter((hook) => !hook.enabled)
|
||||
.map((hook) => getHookDisplayName(hook))
|
||||
.filter((name) => name.startsWith(partialArg));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,13 +242,12 @@ async function enableAllAction(
|
||||
}
|
||||
|
||||
try {
|
||||
const scope = settings.workspace
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooksConfig.disabled', []);
|
||||
|
||||
// Update core config so re-initialization (e.g. extension reload) respects the change
|
||||
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
||||
const scopes = [SettingScope.Workspace, SettingScope.User];
|
||||
for (const scope of scopes) {
|
||||
if (isLoadableSettingScope(scope)) {
|
||||
settings.setValue(scope, 'hooksConfig.disabled', []);
|
||||
}
|
||||
}
|
||||
|
||||
for (const hook of disabledHooks) {
|
||||
const hookName = getHookDisplayName(hook);
|
||||
@@ -325,9 +319,6 @@ async function disableAllAction(
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooksConfig.disabled', allHookNames);
|
||||
|
||||
// Update core config so re-initialization (e.g. extension reload) respects the change
|
||||
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
||||
|
||||
for (const hook of enabledHooks) {
|
||||
const hookName = getHookDisplayName(hook);
|
||||
hookSystem.setHookEnabled(hookName, false);
|
||||
@@ -361,7 +352,7 @@ const enableCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: enableAction,
|
||||
completion: completeHookNames,
|
||||
completion: completeDisabledHookNames,
|
||||
};
|
||||
|
||||
const disableCommand: SlashCommand = {
|
||||
@@ -370,7 +361,7 @@ const disableCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: disableAction,
|
||||
completion: completeHookNames,
|
||||
completion: completeEnabledHookNames,
|
||||
};
|
||||
|
||||
const enableAllCommand: SlashCommand = {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type OpenCustomDialogActionReturn,
|
||||
} from './types.js';
|
||||
import { TriageDuplicates } from '../components/triage/TriageDuplicates.js';
|
||||
import { TriageIssues } from '../components/triage/TriageIssues.js';
|
||||
|
||||
export const oncallCommand: SlashCommand = {
|
||||
name: 'oncall',
|
||||
@@ -49,5 +50,63 @@ export const oncallCommand: SlashCommand = {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'audit',
|
||||
description: 'Triage issues labeled as status/need-triage',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context, args): Promise<OpenCustomDialogActionReturn> => {
|
||||
const { config } = context.services;
|
||||
if (!config) {
|
||||
throw new Error('Config not available');
|
||||
}
|
||||
|
||||
let limit = 100;
|
||||
let until: string | undefined;
|
||||
|
||||
if (args && args.trim().length > 0) {
|
||||
const argArray = args.trim().split(/\s+/);
|
||||
for (let i = 0; i < argArray.length; i++) {
|
||||
const arg = argArray[i];
|
||||
if (arg === '--until') {
|
||||
if (i + 1 >= argArray.length) {
|
||||
throw new Error('Flag --until requires a value (YYYY-MM-DD).');
|
||||
}
|
||||
const val = argArray[i + 1];
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(val)) {
|
||||
throw new Error(
|
||||
`Invalid date format for --until: "${val}". Expected YYYY-MM-DD.`,
|
||||
);
|
||||
}
|
||||
until = val;
|
||||
i++;
|
||||
} else if (arg.startsWith('--')) {
|
||||
throw new Error(`Unknown flag: ${arg}`);
|
||||
} else {
|
||||
const parsedLimit = parseInt(arg, 10);
|
||||
if (!isNaN(parsedLimit) && parsedLimit > 0) {
|
||||
limit = parsedLimit;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid argument: "${arg}". Expected a positive number or --until flag.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'custom_dialog',
|
||||
component: (
|
||||
<TriageIssues
|
||||
config={config}
|
||||
initialLimit={limit}
|
||||
until={until}
|
||||
onExit={() => context.ui.removeComponent()}
|
||||
/>
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CommandKind, type SlashCommand } from './types.js';
|
||||
|
||||
export const shellsCommand: SlashCommand = {
|
||||
name: 'shells',
|
||||
altNames: ['bashes'],
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'Toggle background shells view',
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
context.ui.toggleBackgroundShell();
|
||||
},
|
||||
};
|
||||
@@ -58,6 +58,7 @@ describe('skillsCommand', () => {
|
||||
(name: string) => skills.find((s) => s.name === name) ?? null,
|
||||
),
|
||||
}),
|
||||
getContentGenerator: vi.fn(),
|
||||
} as unknown as Config,
|
||||
settings: {
|
||||
merged: createTestMergedSettings({ skills: { disabled: [] } }),
|
||||
@@ -367,7 +368,7 @@ describe('skillsCommand', () => {
|
||||
expect(context.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Agent skills are disabled by your admin.',
|
||||
text: 'Agent skills is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
@@ -385,7 +386,7 @@ describe('skillsCommand', () => {
|
||||
expect(context.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Agent skills are disabled by your admin.',
|
||||
text: 'Agent skills is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
@@ -11,13 +11,15 @@ import {
|
||||
CommandKind,
|
||||
} from './types.js';
|
||||
import {
|
||||
MessageType,
|
||||
type HistoryItemSkillsList,
|
||||
type HistoryItemInfo,
|
||||
type HistoryItemSkillsList,
|
||||
MessageType,
|
||||
} from '../types.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { enableSkill, disableSkill } from '../../utils/skillSettings.js';
|
||||
import { disableSkill, enableSkill } from '../../utils/skillSettings.js';
|
||||
|
||||
import { getAdminErrorMessage } from '@google/gemini-cli-core';
|
||||
import { renderSkillActionFeedback } from '../../utils/skillUtils.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
|
||||
async function listAction(
|
||||
context: CommandContext,
|
||||
@@ -83,7 +85,10 @@ async function disableAction(
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: 'Agent skills are disabled by your admin.',
|
||||
text: getAdminErrorMessage(
|
||||
'Agent skills',
|
||||
context.services.config ?? undefined,
|
||||
),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
@@ -141,7 +146,10 @@ async function enableAction(
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: 'Agent skills are disabled by your admin.',
|
||||
text: getAdminErrorMessage(
|
||||
'Agent skills',
|
||||
context.services.config ?? undefined,
|
||||
),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,17 @@ import { MessageType } from '../types.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
UserAccountManager: vi.fn().mockImplementation(() => ({
|
||||
getCachedGoogleAccount: vi.fn().mockReturnValue('mock@example.com'),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('statsCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
const startTime = new Date('2025-07-14T10:00:00.000Z');
|
||||
@@ -40,6 +51,9 @@ describe('statsCommand', () => {
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.STATS,
|
||||
duration: expectedDuration,
|
||||
selectedAuthType: '',
|
||||
tier: undefined,
|
||||
userEmail: 'mock@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,8 +62,10 @@ describe('statsCommand', () => {
|
||||
|
||||
const mockQuota = { buckets: [] };
|
||||
const mockRefreshUserQuota = vi.fn().mockResolvedValue(mockQuota);
|
||||
const mockGetUserTierName = vi.fn().mockReturnValue('Basic');
|
||||
mockContext.services.config = {
|
||||
refreshUserQuota: mockRefreshUserQuota,
|
||||
getUserTierName: mockGetUserTierName,
|
||||
} as unknown as Config;
|
||||
|
||||
await statsCommand.action(mockContext, '');
|
||||
@@ -58,6 +74,7 @@ describe('statsCommand', () => {
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
quotas: mockQuota,
|
||||
tier: 'Basic',
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -73,6 +90,9 @@ describe('statsCommand', () => {
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.MODEL_STATS,
|
||||
selectedAuthType: '',
|
||||
tier: undefined,
|
||||
userEmail: 'mock@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,15 +4,33 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { HistoryItemStats } from '../types.js';
|
||||
import type {
|
||||
HistoryItemStats,
|
||||
HistoryItemModelStats,
|
||||
HistoryItemToolStats,
|
||||
} from '../types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
import { UserAccountManager } from '@google/gemini-cli-core';
|
||||
import {
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
CommandKind,
|
||||
} from './types.js';
|
||||
|
||||
function getUserIdentity(context: CommandContext) {
|
||||
const selectedAuthType =
|
||||
context.services.settings.merged.security.auth.selectedType || '';
|
||||
|
||||
const userAccountManager = new UserAccountManager();
|
||||
const cachedAccount = userAccountManager.getCachedGoogleAccount();
|
||||
const userEmail = cachedAccount ?? undefined;
|
||||
|
||||
const tier = context.services.config?.getUserTierName();
|
||||
|
||||
return { selectedAuthType, userEmail, tier };
|
||||
}
|
||||
|
||||
async function defaultSessionView(context: CommandContext) {
|
||||
const now = new Date();
|
||||
const { sessionStartTime } = context.session.stats;
|
||||
@@ -25,9 +43,14 @@ async function defaultSessionView(context: CommandContext) {
|
||||
}
|
||||
const wallDuration = now.getTime() - sessionStartTime.getTime();
|
||||
|
||||
const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
|
||||
|
||||
const statsItem: HistoryItemStats = {
|
||||
type: MessageType.STATS,
|
||||
duration: formatDuration(wallDuration),
|
||||
selectedAuthType,
|
||||
userEmail,
|
||||
tier,
|
||||
};
|
||||
|
||||
if (context.services.config) {
|
||||
@@ -65,9 +88,13 @@ export const statsCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context: CommandContext) => {
|
||||
const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
|
||||
context.ui.addItem({
|
||||
type: MessageType.MODEL_STATS,
|
||||
});
|
||||
selectedAuthType,
|
||||
userEmail,
|
||||
tier,
|
||||
} as HistoryItemModelStats);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -78,7 +105,7 @@ export const statsCommand: SlashCommand = {
|
||||
action: (context: CommandContext) => {
|
||||
context.ui.addItem({
|
||||
type: MessageType.TOOL_STATS,
|
||||
});
|
||||
} as HistoryItemToolStats);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -84,6 +84,7 @@ export interface CommandContext {
|
||||
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void;
|
||||
addConfirmUpdateExtensionRequest: (value: ConfirmationRequest) => void;
|
||||
removeComponent: () => void;
|
||||
toggleBackgroundShell: () => void;
|
||||
};
|
||||
// Session-specific data
|
||||
session: {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { AboutBox } from './AboutBox.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('AboutBox', () => {
|
||||
};
|
||||
|
||||
it('renders with required props', () => {
|
||||
const { lastFrame } = render(<AboutBox {...defaultProps} />);
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...defaultProps} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('About Gemini CLI');
|
||||
expect(output).toContain('1.0.0');
|
||||
@@ -42,7 +42,7 @@ describe('AboutBox', () => {
|
||||
['tier', 'Enterprise', 'Tier'],
|
||||
])('renders optional prop %s', (prop, value, label) => {
|
||||
const props = { ...defaultProps, [prop]: value };
|
||||
const { lastFrame } = render(<AboutBox {...props} />);
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(label);
|
||||
expect(output).toContain(value);
|
||||
@@ -50,14 +50,14 @@ describe('AboutBox', () => {
|
||||
|
||||
it('renders Auth Method with email when userEmail is provided', () => {
|
||||
const props = { ...defaultProps, userEmail: 'test@example.com' };
|
||||
const { lastFrame } = render(<AboutBox {...props} />);
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Logged in with Google (test@example.com)');
|
||||
});
|
||||
|
||||
it('renders Auth Method correctly when not oauth', () => {
|
||||
const props = { ...defaultProps, selectedAuthType: 'api-key' };
|
||||
const { lastFrame } = render(<AboutBox {...props} />);
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('api-key');
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
|
||||
interface AboutBoxProps {
|
||||
cliVersion: string;
|
||||
@@ -31,123 +32,130 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
|
||||
ideClient,
|
||||
userEmail,
|
||||
tier,
|
||||
}) => (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
marginY={1}
|
||||
width="100%"
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
About Gemini CLI
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
CLI Version
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const showUserIdentity = settings.merged.ui.showUserIdentity;
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
marginY={1}
|
||||
width="100%"
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
About Gemini CLI
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{cliVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Git Commit
|
||||
CLI Version
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{GIT_COMMIT_INFO}</Text>
|
||||
<Text color={theme.text.primary}>{cliVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Model
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{modelVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Sandbox
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{sandboxEnv}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
OS
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{osVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Auth Method
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>
|
||||
{selectedAuthType.startsWith('oauth')
|
||||
? userEmail
|
||||
? `Logged in with Google (${userEmail})`
|
||||
: 'Logged in with Google'
|
||||
: selectedAuthType}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{tier && (
|
||||
{GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Git Commit
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{GIT_COMMIT_INFO}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Tier
|
||||
Model
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{tier}</Text>
|
||||
<Text color={theme.text.primary}>{modelVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{gcpProject && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
GCP Project
|
||||
Sandbox
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{gcpProject}</Text>
|
||||
<Text color={theme.text.primary}>{sandboxEnv}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{ideClient && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
IDE Client
|
||||
OS
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{ideClient}</Text>
|
||||
<Text color={theme.text.primary}>{osVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
{showUserIdentity && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Auth Method
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>
|
||||
{selectedAuthType.startsWith('oauth')
|
||||
? userEmail
|
||||
? `Logged in with Google (${userEmail})`
|
||||
: 'Logged in with Google'
|
||||
: selectedAuthType}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{showUserIdentity && tier && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Tier
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{tier}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{gcpProject && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
GCP Project
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{gcpProject}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{ideClient && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
IDE Client
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{ideClient}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,42 +36,3 @@ export const tinyAsciiLogo = `
|
||||
███░ ░░█████████
|
||||
░░░ ░░░░░░░░░
|
||||
`;
|
||||
|
||||
export const shortAsciiLogoIde = `
|
||||
░░░░░░░░░ ░░░░░░░░░░ ░░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░
|
||||
░░░ ░░░ ░░░ ░░░░░░ ░░░░░░ ░░░ ░░░░░░ ░░░░░ ░░░
|
||||
░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░
|
||||
█████████░░██████████ ██████ ░░██████░█████░██████ ░░█████ █████░
|
||||
███░░ ███░███░░ ██████ ░██████░░███░░██████ ░█████ ███░░
|
||||
███░░ ░░███░░ ███░███ ███ ███░░███░░███░███ ███░░ ███░░
|
||||
███░░░░████░██████░░░░░███░░█████ ███░░███░░███░░███ ███░░░ ███░░░
|
||||
███ ███ ███ ███ ███ ███ ███ ███ ██████ ███
|
||||
███ ███ ███ ███ ███ ███ ███ █████ ███
|
||||
█████████ ██████████ ███ ███ █████ ███ █████ █████
|
||||
`;
|
||||
|
||||
export const longAsciiLogoIde = `
|
||||
░░░ ░░░░░░░░░ ░░░░░░░░░░ ░░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░
|
||||
░░░ ░░░ ░░░ ░░░ ░░░░░░ ░░░░░░ ░░░ ░░░░░░ ░░░░░ ░░░
|
||||
░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░
|
||||
███ ░░░ █████████░░██████████ ██████ ░░██████░█████░██████ ░░█████ █████░
|
||||
███ ░░░ ███░ ███░███░░ ██████ ░██████░░███░░██████ ░█████ ███░░
|
||||
███ ███░░░ ░░███░░ ███░███ ███ ███░░███░░███░███ ███░░ ███░░
|
||||
░░░ ███ ███ ░░░█████░██████░░░░░███░░█████ ███░░███░░███░░███ ███░░░ ███░░░
|
||||
███ ███ ███ ███ ███ ███ ███ ███ ███ ██████ ███
|
||||
███ ███ ███ ███ ███ ███ ███ ███ █████ ███
|
||||
███ █████████ ██████████ ███ ███ █████ ███ █████ █████
|
||||
`;
|
||||
|
||||
export const tinyAsciiLogoIde = `
|
||||
░░░ ░░░░░░░░░
|
||||
░░░ ░░░ ░░░
|
||||
░░░ ░░░
|
||||
███ ░░░ █████████░░░
|
||||
███ ░░░ ███░░ ███░░
|
||||
███ ███░░ ░░░
|
||||
░░░ ███ ███░░░░████░
|
||||
███ ███ ███
|
||||
███ ███ ███
|
||||
███ █████████
|
||||
`;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { AskUserDialog } from './AskUserDialog.js';
|
||||
import { QuestionType, type Question } from '@google/gemini-cli-core';
|
||||
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
|
||||
|
||||
// Helper to write to stdin with proper act() wrapping
|
||||
const writeKey = (stdin: { write: (data: string) => void }, key: string) => {
|
||||
@@ -41,6 +42,7 @@ describe('AskUserDialog', () => {
|
||||
questions={authQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -105,6 +107,7 @@ describe('AskUserDialog', () => {
|
||||
questions={questions}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -124,6 +127,7 @@ describe('AskUserDialog', () => {
|
||||
questions={authQuestion}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -153,12 +157,57 @@ describe('AskUserDialog', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ useAlternateBuffer: true, expectedArrows: false },
|
||||
{ useAlternateBuffer: false, expectedArrows: true },
|
||||
])(
|
||||
'Scroll Arrows (useAlternateBuffer: $useAlternateBuffer)',
|
||||
({ useAlternateBuffer, expectedArrows }) => {
|
||||
it(`shows scroll arrows correctly when useAlternateBuffer is ${useAlternateBuffer}`, async () => {
|
||||
const questions: Question[] = [
|
||||
{
|
||||
question: 'Choose an option',
|
||||
header: 'Scroll Test',
|
||||
options: Array.from({ length: 15 }, (_, i) => ({
|
||||
label: `Option ${i + 1}`,
|
||||
description: `Description ${i + 1}`,
|
||||
})),
|
||||
multiSelect: false,
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={80}
|
||||
availableHeight={10} // Small height to force scrolling
|
||||
/>,
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
if (expectedArrows) {
|
||||
expect(lastFrame()).toContain('▲');
|
||||
expect(lastFrame()).toContain('▼');
|
||||
} else {
|
||||
expect(lastFrame()).not.toContain('▲');
|
||||
expect(lastFrame()).not.toContain('▼');
|
||||
}
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('navigates to custom option when typing unbound characters (Type-to-Jump)', async () => {
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={authQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -209,6 +258,7 @@ describe('AskUserDialog', () => {
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -222,6 +272,7 @@ describe('AskUserDialog', () => {
|
||||
questions={authQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -235,6 +286,7 @@ describe('AskUserDialog', () => {
|
||||
questions={authQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -265,6 +317,7 @@ describe('AskUserDialog', () => {
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -306,6 +359,7 @@ describe('AskUserDialog', () => {
|
||||
questions={multiQuestions}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -373,6 +427,7 @@ describe('AskUserDialog', () => {
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -401,6 +456,7 @@ describe('AskUserDialog', () => {
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -445,6 +501,7 @@ describe('AskUserDialog', () => {
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -480,6 +537,7 @@ describe('AskUserDialog', () => {
|
||||
questions={multiQuestions}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -512,6 +570,7 @@ describe('AskUserDialog', () => {
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -533,6 +592,7 @@ describe('AskUserDialog', () => {
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -554,6 +614,7 @@ describe('AskUserDialog', () => {
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -588,6 +649,7 @@ describe('AskUserDialog', () => {
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -618,6 +680,7 @@ describe('AskUserDialog', () => {
|
||||
questions={mixedQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -664,6 +727,7 @@ describe('AskUserDialog', () => {
|
||||
questions={mixedQuestions}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -713,6 +777,7 @@ describe('AskUserDialog', () => {
|
||||
questions={textQuestion}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -738,6 +803,7 @@ describe('AskUserDialog', () => {
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={onCancel}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -783,6 +849,7 @@ describe('AskUserDialog', () => {
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -841,6 +908,7 @@ describe('AskUserDialog', () => {
|
||||
questions={multiQuestions}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
@@ -872,4 +940,139 @@ describe('AskUserDialog', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('uses availableTerminalHeight from UIStateContext if availableHeight prop is missing', () => {
|
||||
const questions: Question[] = [
|
||||
{
|
||||
question: 'Choose an option',
|
||||
header: 'Context Test',
|
||||
options: Array.from({ length: 10 }, (_, i) => ({
|
||||
label: `Option ${i + 1}`,
|
||||
description: `Description ${i + 1}`,
|
||||
})),
|
||||
multiSelect: false,
|
||||
},
|
||||
];
|
||||
|
||||
const mockUIState = {
|
||||
availableTerminalHeight: 5, // Small height to force scroll arrows
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<UIStateContext.Provider value={mockUIState}>
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={80}
|
||||
/>
|
||||
</UIStateContext.Provider>,
|
||||
{ useAlternateBuffer: false },
|
||||
);
|
||||
|
||||
// With height 5 and alternate buffer disabled, it should show scroll arrows (▲)
|
||||
expect(lastFrame()).toContain('▲');
|
||||
expect(lastFrame()).toContain('▼');
|
||||
});
|
||||
|
||||
it('does NOT truncate the question when in alternate buffer mode even with small height', () => {
|
||||
const longQuestion =
|
||||
'This is a very long question ' + 'with many words '.repeat(10);
|
||||
const questions: Question[] = [
|
||||
{
|
||||
question: longQuestion,
|
||||
header: 'Alternate Buffer Test',
|
||||
options: [{ label: 'Option 1', description: 'Desc 1' }],
|
||||
multiSelect: false,
|
||||
},
|
||||
];
|
||||
|
||||
const mockUIState = {
|
||||
availableTerminalHeight: 5,
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<UIStateContext.Provider value={mockUIState}>
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={40} // Small width to force wrapping
|
||||
/>
|
||||
</UIStateContext.Provider>,
|
||||
{ useAlternateBuffer: true },
|
||||
);
|
||||
|
||||
// Should NOT contain the truncation message
|
||||
expect(lastFrame()).not.toContain('hidden ...');
|
||||
// Should contain the full long question (or at least its parts)
|
||||
expect(lastFrame()).toContain('This is a very long question');
|
||||
});
|
||||
|
||||
describe('Choice question placeholder', () => {
|
||||
it('uses placeholder for "Other" option when provided', async () => {
|
||||
const questions: Question[] = [
|
||||
{
|
||||
question: 'Select your preferred language:',
|
||||
header: 'Language',
|
||||
options: [
|
||||
{ label: 'TypeScript', description: '' },
|
||||
{ label: 'JavaScript', description: '' },
|
||||
],
|
||||
placeholder: 'Type another language...',
|
||||
multiSelect: false,
|
||||
},
|
||||
];
|
||||
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={80}
|
||||
/>,
|
||||
{ width: 80 },
|
||||
);
|
||||
|
||||
// Navigate to the "Other" option
|
||||
writeKey(stdin, '\x1b[B'); // Down
|
||||
writeKey(stdin, '\x1b[B'); // Down to Other
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('uses default placeholder when not provided', async () => {
|
||||
const questions: Question[] = [
|
||||
{
|
||||
question: 'Select your preferred language:',
|
||||
header: 'Language',
|
||||
options: [
|
||||
{ label: 'TypeScript', description: '' },
|
||||
{ label: 'JavaScript', description: '' },
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
];
|
||||
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={80}
|
||||
/>,
|
||||
{ width: 80 },
|
||||
);
|
||||
|
||||
// Navigate to the "Other" option
|
||||
writeKey(stdin, '\x1b[B'); // Down
|
||||
writeKey(stdin, '\x1b[B'); // Down to Other
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,10 +24,12 @@ import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { checkExhaustive } from '../../utils/checks.js';
|
||||
import { TextInput } from './shared/TextInput.js';
|
||||
import { useTextBuffer } from './shared/text-buffer.js';
|
||||
import { UIStateContext } from '../contexts/UIStateContext.js';
|
||||
import { getCachedStringWidth } from '../utils/textUtils.js';
|
||||
import { useTabbedNavigation } from '../hooks/useTabbedNavigation.js';
|
||||
import { DialogFooter } from './shared/DialogFooter.js';
|
||||
import { MaxSizedBox } from './shared/MaxSizedBox.js';
|
||||
import { UIStateContext } from '../contexts/UIStateContext.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
|
||||
interface AskUserDialogState {
|
||||
answers: { [key: string]: string };
|
||||
@@ -121,6 +123,14 @@ interface AskUserDialogProps {
|
||||
* Useful for managing global keypress handlers.
|
||||
*/
|
||||
onActiveTextInputChange?: (active: boolean) => void;
|
||||
/**
|
||||
* Width of the dialog.
|
||||
*/
|
||||
width: number;
|
||||
/**
|
||||
* Height constraint for scrollable content.
|
||||
*/
|
||||
availableHeight?: number;
|
||||
}
|
||||
|
||||
interface ReviewViewProps {
|
||||
@@ -152,12 +162,7 @@ const ReviewView: React.FC<ReviewViewProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
paddingX={1}
|
||||
borderColor={theme.border.default}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
{progressHeader}
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
@@ -174,15 +179,19 @@ const ReviewView: React.FC<ReviewViewProps> = ({
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{questions.map((q, i) => (
|
||||
<Box key={i} marginBottom={0}>
|
||||
<Text color={theme.text.secondary}>{q.header}</Text>
|
||||
<Text color={theme.text.secondary}> → </Text>
|
||||
<Text color={answers[i] ? theme.text.primary : theme.status.warning}>
|
||||
{answers[i] || '(not answered)'}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
<Box flexDirection="column">
|
||||
{questions.map((q, i) => (
|
||||
<Box key={i} marginBottom={0}>
|
||||
<Text color={theme.text.secondary}>{q.header}</Text>
|
||||
<Text color={theme.text.secondary}> → </Text>
|
||||
<Text
|
||||
color={answers[i] ? theme.text.primary : theme.status.warning}
|
||||
>
|
||||
{answers[i] || '(not answered)'}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<DialogFooter
|
||||
primaryAction="Enter to submit"
|
||||
navigationActions="Tab/Shift+Tab to edit answers"
|
||||
@@ -199,6 +208,7 @@ interface TextQuestionViewProps {
|
||||
onSelectionChange?: (answer: string) => void;
|
||||
onEditingCustomOption?: (editing: boolean) => void;
|
||||
availableWidth: number;
|
||||
availableHeight?: number;
|
||||
initialAnswer?: string;
|
||||
progressHeader?: React.ReactNode;
|
||||
keyboardHints?: React.ReactNode;
|
||||
@@ -210,12 +220,14 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
|
||||
onSelectionChange,
|
||||
onEditingCustomOption,
|
||||
availableWidth,
|
||||
availableHeight,
|
||||
initialAnswer,
|
||||
progressHeader,
|
||||
keyboardHints,
|
||||
}) => {
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const prefix = '> ';
|
||||
const horizontalPadding = 4 + 1; // Padding from Box (2) and border (2) + 1 for cursor
|
||||
const horizontalPadding = 1; // 1 for cursor
|
||||
const bufferWidth =
|
||||
availableWidth - getCachedStringWidth(prefix) - horizontalPadding;
|
||||
|
||||
@@ -241,12 +253,15 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
|
||||
const handleExtraKeys = useCallback(
|
||||
(key: Key) => {
|
||||
if (keyMatchers[Command.QUIT](key)) {
|
||||
if (textValue === '') {
|
||||
return false;
|
||||
}
|
||||
buffer.setText('');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[buffer],
|
||||
[buffer, textValue],
|
||||
);
|
||||
|
||||
useKeypress(handleExtraKeys, { isActive: true, priority: true });
|
||||
@@ -270,18 +285,28 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
|
||||
|
||||
const placeholder = question.placeholder || 'Enter your response';
|
||||
|
||||
const HEADER_HEIGHT = progressHeader ? 2 : 0;
|
||||
const INPUT_HEIGHT = 2; // TextInput + margin
|
||||
const FOOTER_HEIGHT = 2; // DialogFooter + margin
|
||||
const overhead = HEADER_HEIGHT + INPUT_HEIGHT + FOOTER_HEIGHT;
|
||||
const questionHeight =
|
||||
availableHeight && !isAlternateBuffer
|
||||
? Math.max(1, availableHeight - overhead)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
paddingX={1}
|
||||
borderColor={theme.border.default}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
{progressHeader}
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{question.question}
|
||||
</Text>
|
||||
<MaxSizedBox
|
||||
maxHeight={questionHeight}
|
||||
maxWidth={availableWidth}
|
||||
overflowDirection="bottom"
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{question.question}
|
||||
</Text>
|
||||
</MaxSizedBox>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
@@ -381,6 +406,7 @@ interface ChoiceQuestionViewProps {
|
||||
onSelectionChange?: (answer: string) => void;
|
||||
onEditingCustomOption?: (editing: boolean) => void;
|
||||
availableWidth: number;
|
||||
availableHeight?: number;
|
||||
initialAnswer?: string;
|
||||
progressHeader?: React.ReactNode;
|
||||
keyboardHints?: React.ReactNode;
|
||||
@@ -391,14 +417,13 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
onAnswer,
|
||||
onSelectionChange,
|
||||
onEditingCustomOption,
|
||||
availableWidth,
|
||||
availableHeight,
|
||||
initialAnswer,
|
||||
progressHeader,
|
||||
keyboardHints,
|
||||
}) => {
|
||||
const uiState = useContext(UIStateContext);
|
||||
const terminalWidth = uiState?.terminalWidth ?? 80;
|
||||
const availableWidth = terminalWidth;
|
||||
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const numOptions =
|
||||
(question.options?.length ?? 0) + (question.type !== 'yesno' ? 1 : 0);
|
||||
const numLen = String(numOptions).length;
|
||||
@@ -407,15 +432,9 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
const checkboxWidth = question.multiSelect ? 4 : 1; // "[x] " or " "
|
||||
const checkmarkWidth = question.multiSelect ? 0 : 2; // "" or " ✓"
|
||||
const cursorPadding = 1; // Extra character for cursor at end of line
|
||||
const outerBoxPadding = 4; // border (2) + paddingX (2)
|
||||
|
||||
const horizontalPadding =
|
||||
outerBoxPadding +
|
||||
radioWidth +
|
||||
numberWidth +
|
||||
checkboxWidth +
|
||||
checkmarkWidth +
|
||||
cursorPadding;
|
||||
radioWidth + numberWidth + checkboxWidth + checkmarkWidth + cursorPadding;
|
||||
|
||||
const bufferWidth = availableWidth - horizontalPadding;
|
||||
|
||||
@@ -544,6 +563,9 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
(key: Key) => {
|
||||
// If focusing custom option, handle Ctrl+C
|
||||
if (isCustomOptionFocused && keyMatchers[Command.QUIT](key)) {
|
||||
if (customOptionText === '') {
|
||||
return false;
|
||||
}
|
||||
customBuffer.setText('');
|
||||
return true;
|
||||
}
|
||||
@@ -586,7 +608,12 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[isCustomOptionFocused, customBuffer, onEditingCustomOption],
|
||||
[
|
||||
isCustomOptionFocused,
|
||||
customBuffer,
|
||||
onEditingCustomOption,
|
||||
customOptionText,
|
||||
],
|
||||
);
|
||||
|
||||
useKeypress(handleExtraKeys, { isActive: true, priority: true });
|
||||
@@ -698,31 +725,50 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
}
|
||||
}, [customOptionText, isCustomOptionSelected, question.multiSelect]);
|
||||
|
||||
const HEADER_HEIGHT = progressHeader ? 2 : 0;
|
||||
const TITLE_MARGIN = 1;
|
||||
const FOOTER_HEIGHT = 2; // DialogFooter + margin
|
||||
const overhead = HEADER_HEIGHT + TITLE_MARGIN + FOOTER_HEIGHT;
|
||||
const listHeight = availableHeight
|
||||
? Math.max(1, availableHeight - overhead)
|
||||
: undefined;
|
||||
const questionHeight =
|
||||
listHeight && !isAlternateBuffer
|
||||
? Math.min(15, Math.max(1, listHeight - 4))
|
||||
: undefined;
|
||||
const maxItemsToShow =
|
||||
listHeight && questionHeight
|
||||
? Math.max(1, Math.floor((listHeight - questionHeight) / 2))
|
||||
: selectionItems.length;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
paddingX={1}
|
||||
borderColor={theme.border.default}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
{progressHeader}
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{question.question}
|
||||
</Text>
|
||||
<Box marginBottom={TITLE_MARGIN}>
|
||||
<MaxSizedBox
|
||||
maxHeight={questionHeight}
|
||||
maxWidth={availableWidth}
|
||||
overflowDirection="bottom"
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{question.question}
|
||||
{question.multiSelect && (
|
||||
<Text color={theme.text.secondary} italic>
|
||||
{' '}
|
||||
(Select all that apply)
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</MaxSizedBox>
|
||||
</Box>
|
||||
{question.multiSelect && (
|
||||
<Text color={theme.text.secondary} italic>
|
||||
{' '}
|
||||
(Select all that apply)
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<BaseSelectionList<OptionItem>
|
||||
items={selectionItems}
|
||||
onSelect={handleSelect}
|
||||
onHighlight={handleHighlight}
|
||||
focusKey={isCustomOptionFocused ? 'other' : undefined}
|
||||
maxItemsToShow={maxItemsToShow}
|
||||
showScrollArrows={true}
|
||||
renderItem={(item, context) => {
|
||||
const optionItem = item.value;
|
||||
const isChecked =
|
||||
@@ -734,7 +780,7 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
|
||||
// Render inline text input for custom option
|
||||
if (optionItem.type === 'other') {
|
||||
const placeholder = 'Enter a custom value';
|
||||
const placeholder = question.placeholder || 'Enter a custom value';
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
{showCheck && (
|
||||
@@ -804,14 +850,19 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
onActiveTextInputChange,
|
||||
width,
|
||||
availableHeight: availableHeightProp,
|
||||
}) => {
|
||||
const uiState = useContext(UIStateContext);
|
||||
const availableHeight =
|
||||
availableHeightProp ??
|
||||
(uiState?.constrainHeight !== false
|
||||
? uiState?.availableTerminalHeight
|
||||
: undefined);
|
||||
|
||||
const [state, dispatch] = useReducer(askUserDialogReducerLogic, initialState);
|
||||
const { answers, isEditingCustomOption, submitted } = state;
|
||||
|
||||
const uiState = useContext(UIStateContext);
|
||||
const terminalWidth = uiState?.terminalWidth ?? 80;
|
||||
const availableWidth = terminalWidth;
|
||||
|
||||
const reviewTabIndex = questions.length;
|
||||
const tabCount =
|
||||
questions.length > 1 ? questions.length + 1 : questions.length;
|
||||
@@ -842,9 +893,12 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
||||
if (keyMatchers[Command.ESCAPE](key)) {
|
||||
onCancel();
|
||||
return true;
|
||||
} else if (keyMatchers[Command.QUIT](key) && !isEditingCustomOption) {
|
||||
onCancel();
|
||||
return true;
|
||||
} else if (keyMatchers[Command.QUIT](key)) {
|
||||
if (!isEditingCustomOption) {
|
||||
onCancel();
|
||||
}
|
||||
// Return false to let ctrl-C bubble up to AppContainer for exit flow
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -1021,7 +1075,8 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
||||
onAnswer={handleAnswer}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
onEditingCustomOption={handleEditingCustomOption}
|
||||
availableWidth={availableWidth}
|
||||
availableWidth={width}
|
||||
availableHeight={availableHeight}
|
||||
initialAnswer={answers[currentQuestionIndex]}
|
||||
progressHeader={progressHeader}
|
||||
keyboardHints={keyboardHints}
|
||||
@@ -1033,7 +1088,8 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
||||
onAnswer={handleAnswer}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
onEditingCustomOption={handleEditingCustomOption}
|
||||
availableWidth={availableWidth}
|
||||
availableWidth={width}
|
||||
availableHeight={availableHeight}
|
||||
initialAnswer={answers[currentQuestionIndex]}
|
||||
progressHeader={progressHeader}
|
||||
keyboardHints={keyboardHints}
|
||||
@@ -1043,7 +1099,7 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
width={availableWidth}
|
||||
width={width}
|
||||
aria-label={`Question ${currentQuestionIndex + 1} of ${questions.length}: ${currentQuestion.question}`}
|
||||
>
|
||||
{questionView}
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
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';
|
||||
import { ScrollProvider } from '../contexts/ScrollProvider.js';
|
||||
import { Box } from 'ink';
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Mock dependencies
|
||||
const mockDismissBackgroundShell = vi.fn();
|
||||
const mockSetActiveBackgroundShellPid = vi.fn();
|
||||
const mockSetIsBackgroundShellListOpen = vi.fn();
|
||||
const mockHandleWarning = vi.fn();
|
||||
const mockSetEmbeddedShellFocused = vi.fn();
|
||||
|
||||
vi.mock('../contexts/UIActionsContext.js', () => ({
|
||||
useUIActions: () => ({
|
||||
dismissBackgroundShell: mockDismissBackgroundShell,
|
||||
setActiveBackgroundShellPid: mockSetActiveBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen: mockSetIsBackgroundShellListOpen,
|
||||
handleWarning: mockHandleWarning,
|
||||
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
ShellExecutionService: {
|
||||
resizePty: vi.fn(),
|
||||
subscribe: vi.fn(() => vi.fn()),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock AnsiOutputText since it's a complex component
|
||||
vi.mock('./AnsiOutput.js', () => ({
|
||||
AnsiOutputText: ({ data }: { data: string | unknown }) => {
|
||||
if (typeof data === 'string') return <>{data}</>;
|
||||
// Simple serialization for object data
|
||||
return <>{JSON.stringify(data)}</>;
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock useKeypress
|
||||
let keypressHandlers: Array<{ handler: KeypressHandler; isActive: boolean }> =
|
||||
[];
|
||||
vi.mock('../hooks/useKeypress.js', () => ({
|
||||
useKeypress: vi.fn((handler, { isActive }) => {
|
||||
keypressHandlers.push({ handler, isActive });
|
||||
}),
|
||||
}));
|
||||
|
||||
const simulateKey = (key: Partial<Key>) => {
|
||||
const fullKey: Key = createMockKey(key);
|
||||
keypressHandlers.forEach(({ handler, isActive }) => {
|
||||
if (isActive) {
|
||||
handler(fullKey);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
vi.mock('../contexts/MouseContext.js', () => ({
|
||||
useMouseContext: vi.fn(() => ({
|
||||
subscribe: vi.fn(),
|
||||
unsubscribe: vi.fn(),
|
||||
})),
|
||||
useMouse: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock ScrollableList
|
||||
vi.mock('./shared/ScrollableList.js', () => ({
|
||||
SCROLL_TO_ITEM_END: 999999,
|
||||
ScrollableList: vi.fn(
|
||||
({
|
||||
data,
|
||||
renderItem,
|
||||
}: {
|
||||
data: BackgroundShell[];
|
||||
renderItem: (props: {
|
||||
item: BackgroundShell;
|
||||
index: number;
|
||||
}) => React.ReactNode;
|
||||
}) => (
|
||||
<Box flexDirection="column">
|
||||
{data.map((item: BackgroundShell, index: number) => (
|
||||
<Box key={index}>{renderItem({ item, index })}</Box>
|
||||
))}
|
||||
</Box>
|
||||
),
|
||||
),
|
||||
}));
|
||||
|
||||
const createMockKey = (overrides: Partial<Key>): Key => ({
|
||||
name: '',
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
cmd: false,
|
||||
shift: false,
|
||||
insertable: false,
|
||||
sequence: '',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('<BackgroundShellDisplay />', () => {
|
||||
const mockShells = new Map<number, BackgroundShell>();
|
||||
const shell1: BackgroundShell = {
|
||||
pid: 1001,
|
||||
command: 'npm start',
|
||||
output: 'Starting server...',
|
||||
isBinary: false,
|
||||
binaryBytesReceived: 0,
|
||||
status: 'running',
|
||||
};
|
||||
const shell2: BackgroundShell = {
|
||||
pid: 1002,
|
||||
command: 'tail -f log.txt',
|
||||
output: 'Log entry 1',
|
||||
isBinary: false,
|
||||
binaryBytesReceived: 0,
|
||||
status: 'running',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockShells.clear();
|
||||
mockShells.set(shell1.pid, shell1);
|
||||
mockShells.set(shell2.pid, shell2);
|
||||
keypressHandlers = [];
|
||||
});
|
||||
|
||||
it('renders the output of the active shell', async () => {
|
||||
const { lastFrame } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
height={24}
|
||||
isFocused={false}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders tabs for multiple shells', async () => {
|
||||
const { lastFrame } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={100}
|
||||
height={24}
|
||||
isFocused={false}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('highlights the focused state', async () => {
|
||||
const { lastFrame } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
height={24}
|
||||
isFocused={true} // Focused
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('resizes the PTY on mount and when dimensions change', async () => {
|
||||
const { rerender } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
height={24}
|
||||
isFocused={false}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
expect(ShellExecutionService.resizePty).toHaveBeenCalledWith(
|
||||
shell1.pid,
|
||||
76,
|
||||
21,
|
||||
);
|
||||
|
||||
rerender(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={100}
|
||||
height={30}
|
||||
isFocused={false}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
expect(ShellExecutionService.resizePty).toHaveBeenCalledWith(
|
||||
shell1.pid,
|
||||
96,
|
||||
27,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the process list when isListOpenProp is true', async () => {
|
||||
const { lastFrame } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={true}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('selects the current process and closes the list when Ctrl+L is pressed in list view', async () => {
|
||||
render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={true}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
// Simulate down arrow to select the second process (handled by RadioButtonSelect)
|
||||
act(() => {
|
||||
simulateKey({ name: 'down' });
|
||||
});
|
||||
|
||||
// Simulate Ctrl+L (handled by BackgroundShellDisplay)
|
||||
act(() => {
|
||||
simulateKey({ name: 'l', ctrl: true });
|
||||
});
|
||||
|
||||
expect(mockSetActiveBackgroundShellPid).toHaveBeenCalledWith(shell2.pid);
|
||||
expect(mockSetIsBackgroundShellListOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('kills the highlighted process when Ctrl+K is pressed in list view', async () => {
|
||||
render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={true}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
// Initial state: shell1 (active) is highlighted
|
||||
|
||||
// Move to shell2
|
||||
act(() => {
|
||||
simulateKey({ name: 'down' });
|
||||
});
|
||||
|
||||
// Press Ctrl+K
|
||||
act(() => {
|
||||
simulateKey({ name: 'k', ctrl: true });
|
||||
});
|
||||
|
||||
expect(mockDismissBackgroundShell).toHaveBeenCalledWith(shell2.pid);
|
||||
});
|
||||
|
||||
it('kills the active process when Ctrl+K is pressed in output view', async () => {
|
||||
render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
simulateKey({ name: 'k', ctrl: true });
|
||||
});
|
||||
|
||||
expect(mockDismissBackgroundShell).toHaveBeenCalledWith(shell1.pid);
|
||||
});
|
||||
|
||||
it('scrolls to active shell when list opens', async () => {
|
||||
// shell2 is active
|
||||
const { lastFrame } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell2.pid}
|
||||
width={80}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={true}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('keeps exit code status color even when selected', async () => {
|
||||
const exitedShell: BackgroundShell = {
|
||||
pid: 1003,
|
||||
command: 'exit 0',
|
||||
output: '',
|
||||
isBinary: false,
|
||||
binaryBytesReceived: 0,
|
||||
status: 'exited',
|
||||
exitCode: 0,
|
||||
};
|
||||
mockShells.set(exitedShell.pid, exitedShell);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={exitedShell.pid}
|
||||
width={80}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={true}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('unfocuses the shell when Shift+Tab is pressed', async () => {
|
||||
render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
simulateKey({ name: 'tab', shift: true });
|
||||
});
|
||||
|
||||
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('shows a warning when Tab is pressed', async () => {
|
||||
render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
simulateKey({ name: 'tab' });
|
||||
});
|
||||
|
||||
expect(mockHandleWarning).toHaveBeenCalledWith(
|
||||
'Press Shift+Tab to focus out.',
|
||||
);
|
||||
expect(mockSetEmbeddedShellFocused).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,460 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import {
|
||||
ShellExecutionService,
|
||||
type AnsiOutput,
|
||||
type AnsiLine,
|
||||
type AnsiToken,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { cpLen, cpSlice, getCachedStringWidth } from '../utils/textUtils.js';
|
||||
import { type BackgroundShell } from '../hooks/shellCommandProcessor.js';
|
||||
import { Command, keyMatchers } from '../keyMatchers.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { commandDescriptions } from '../../config/keyBindings.js';
|
||||
import {
|
||||
ScrollableList,
|
||||
type ScrollableListRef,
|
||||
} from './shared/ScrollableList.js';
|
||||
|
||||
import { SCROLL_TO_ITEM_END } from './shared/VirtualizedList.js';
|
||||
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
|
||||
interface BackgroundShellDisplayProps {
|
||||
shells: Map<number, BackgroundShell>;
|
||||
activePid: number;
|
||||
width: number;
|
||||
height: number;
|
||||
isFocused: boolean;
|
||||
isListOpenProp: boolean;
|
||||
}
|
||||
|
||||
const CONTENT_PADDING_X = 1;
|
||||
const BORDER_WIDTH = 2; // Left and Right border
|
||||
const HEADER_HEIGHT = 3; // 2 for border, 1 for header
|
||||
const TAB_DISPLAY_HORIZONTAL_PADDING = 4;
|
||||
|
||||
const formatShellCommandForDisplay = (command: string, maxWidth: number) => {
|
||||
const commandFirstLine = command.split('\n')[0];
|
||||
return cpLen(commandFirstLine) > maxWidth
|
||||
? `${cpSlice(commandFirstLine, 0, maxWidth - 3)}...`
|
||||
: commandFirstLine;
|
||||
};
|
||||
|
||||
export const BackgroundShellDisplay = ({
|
||||
shells,
|
||||
activePid,
|
||||
width,
|
||||
height,
|
||||
isFocused,
|
||||
isListOpenProp,
|
||||
}: BackgroundShellDisplayProps) => {
|
||||
const {
|
||||
dismissBackgroundShell,
|
||||
setActiveBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
handleWarning,
|
||||
setEmbeddedShellFocused,
|
||||
} = useUIActions();
|
||||
const activeShell = shells.get(activePid);
|
||||
const [output, setOutput] = useState<string | AnsiOutput>(
|
||||
activeShell?.output || '',
|
||||
);
|
||||
const [highlightedPid, setHighlightedPid] = useState<number | null>(
|
||||
activePid,
|
||||
);
|
||||
const outputRef = useRef<ScrollableListRef<AnsiLine | string>>(null);
|
||||
const subscribedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activePid) return;
|
||||
|
||||
const ptyWidth = Math.max(1, width - BORDER_WIDTH - CONTENT_PADDING_X * 2);
|
||||
const ptyHeight = Math.max(1, height - HEADER_HEIGHT);
|
||||
ShellExecutionService.resizePty(activePid, ptyWidth, ptyHeight);
|
||||
}, [activePid, width, height]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activePid) {
|
||||
setOutput('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set initial output from the shell object
|
||||
const shell = shells.get(activePid);
|
||||
if (shell) {
|
||||
setOutput(shell.output);
|
||||
}
|
||||
|
||||
subscribedRef.current = false;
|
||||
|
||||
// Subscribe to live updates for the active shell
|
||||
const unsubscribe = ShellExecutionService.subscribe(activePid, (event) => {
|
||||
if (event.type === 'data') {
|
||||
if (typeof event.chunk === 'string') {
|
||||
if (!subscribedRef.current) {
|
||||
// Initial synchronous update contains full history
|
||||
setOutput(event.chunk);
|
||||
} else {
|
||||
// Subsequent updates are deltas for child_process
|
||||
setOutput((prev) =>
|
||||
typeof prev === 'string' ? prev + event.chunk : event.chunk,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// PTY always sends full AnsiOutput
|
||||
setOutput(event.chunk);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
subscribedRef.current = true;
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
subscribedRef.current = false;
|
||||
};
|
||||
}, [activePid, shells]);
|
||||
|
||||
// Sync highlightedPid with activePid when list opens
|
||||
useEffect(() => {
|
||||
if (isListOpenProp) {
|
||||
setHighlightedPid(activePid);
|
||||
}
|
||||
}, [isListOpenProp, activePid]);
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (!activeShell) return;
|
||||
|
||||
// Handle Shift+Tab or Tab (in list) to focus out
|
||||
if (
|
||||
keyMatchers[Command.UNFOCUS_BACKGROUND_SHELL](key) ||
|
||||
(isListOpenProp &&
|
||||
keyMatchers[Command.UNFOCUS_BACKGROUND_SHELL_LIST](key))
|
||||
) {
|
||||
setEmbeddedShellFocused(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle Tab to warn but propagate
|
||||
if (
|
||||
!isListOpenProp &&
|
||||
keyMatchers[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING](key)
|
||||
) {
|
||||
handleWarning(
|
||||
`Press ${commandDescriptions[Command.UNFOCUS_BACKGROUND_SHELL]} to focus out.`,
|
||||
);
|
||||
// Fall through to allow Tab to be sent to the shell
|
||||
}
|
||||
|
||||
if (isListOpenProp) {
|
||||
// Navigation (Up/Down/Enter) is handled by RadioButtonSelect
|
||||
// We only handle special keys not consumed by RadioButtonSelect or overriding them if needed
|
||||
// RadioButtonSelect handles Enter -> onSelect
|
||||
|
||||
if (keyMatchers[Command.BACKGROUND_SHELL_ESCAPE](key)) {
|
||||
setIsBackgroundShellListOpen(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.KILL_BACKGROUND_SHELL](key)) {
|
||||
if (highlightedPid) {
|
||||
dismissBackgroundShell(highlightedPid);
|
||||
// If we killed the active one, the list might update via props
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL_LIST](key)) {
|
||||
if (highlightedPid) {
|
||||
setActiveBackgroundShellPid(highlightedPid);
|
||||
}
|
||||
setIsBackgroundShellListOpen(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL](key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.KILL_BACKGROUND_SHELL](key)) {
|
||||
dismissBackgroundShell(activeShell.pid);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL_LIST](key)) {
|
||||
setIsBackgroundShellListOpen(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.BACKGROUND_SHELL_SELECT](key)) {
|
||||
ShellExecutionService.writeToPty(activeShell.pid, '\r');
|
||||
return true;
|
||||
} else if (keyMatchers[Command.DELETE_CHAR_LEFT](key)) {
|
||||
ShellExecutionService.writeToPty(activeShell.pid, '\b');
|
||||
return true;
|
||||
} else if (key.sequence) {
|
||||
ShellExecutionService.writeToPty(activeShell.pid, key.sequence);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ isActive: isFocused && !!activeShell },
|
||||
);
|
||||
|
||||
const helpText = `${commandDescriptions[Command.TOGGLE_BACKGROUND_SHELL]} Hide | ${commandDescriptions[Command.KILL_BACKGROUND_SHELL]} Kill | ${commandDescriptions[Command.TOGGLE_BACKGROUND_SHELL_LIST]} List`;
|
||||
|
||||
const renderTabs = () => {
|
||||
const shellList = Array.from(shells.values()).filter(
|
||||
(s) => s.status === 'running',
|
||||
);
|
||||
|
||||
const pidInfoWidth = getCachedStringWidth(
|
||||
` (PID: ${activePid}) ${isFocused ? '(Focused)' : ''}`,
|
||||
);
|
||||
|
||||
const availableWidth =
|
||||
width -
|
||||
TAB_DISPLAY_HORIZONTAL_PADDING -
|
||||
getCachedStringWidth(helpText) -
|
||||
pidInfoWidth;
|
||||
|
||||
let currentWidth = 0;
|
||||
const tabs = [];
|
||||
|
||||
for (let i = 0; i < shellList.length; i++) {
|
||||
const shell = shellList[i];
|
||||
// Account for " i: " (length 4 if i < 9) and spaces (length 2)
|
||||
const labelOverhead = 4 + (i + 1).toString().length;
|
||||
const maxTabLabelLength = Math.max(
|
||||
1,
|
||||
Math.floor(availableWidth / shellList.length) - labelOverhead,
|
||||
);
|
||||
const truncatedCommand = formatShellCommandForDisplay(
|
||||
shell.command,
|
||||
maxTabLabelLength,
|
||||
);
|
||||
const label = ` ${i + 1}: ${truncatedCommand} `;
|
||||
const labelWidth = getCachedStringWidth(label);
|
||||
|
||||
// If this is the only shell, we MUST show it (truncated if necessary)
|
||||
// even if it exceeds availableWidth, as there are no alternatives.
|
||||
if (i > 0 && currentWidth + labelWidth > availableWidth) {
|
||||
break;
|
||||
}
|
||||
|
||||
const isActive = shell.pid === activePid;
|
||||
|
||||
tabs.push(
|
||||
<Text
|
||||
key={shell.pid}
|
||||
color={isActive ? theme.text.primary : theme.text.secondary}
|
||||
bold={isActive}
|
||||
>
|
||||
{label}
|
||||
</Text>,
|
||||
);
|
||||
currentWidth += labelWidth;
|
||||
}
|
||||
|
||||
if (shellList.length > tabs.length && !isListOpenProp) {
|
||||
const overflowLabel = ` ... (${commandDescriptions[Command.TOGGLE_BACKGROUND_SHELL_LIST]}) `;
|
||||
const overflowWidth = getCachedStringWidth(overflowLabel);
|
||||
|
||||
// If we only have one tab, ensure we don't show the overflow if it's too cramped
|
||||
// We want at least 10 chars for the overflow or we favor the first tab.
|
||||
const shouldShowOverflow =
|
||||
tabs.length > 1 || availableWidth - currentWidth >= overflowWidth;
|
||||
|
||||
if (shouldShowOverflow) {
|
||||
tabs.push(
|
||||
<Text key="overflow" color={theme.status.warning} bold>
|
||||
{overflowLabel}
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return tabs;
|
||||
};
|
||||
|
||||
const renderProcessList = () => {
|
||||
const maxCommandLength = Math.max(
|
||||
0,
|
||||
width - BORDER_WIDTH - CONTENT_PADDING_X * 2 - 10,
|
||||
);
|
||||
|
||||
const items: Array<RadioSelectItem<number>> = Array.from(
|
||||
shells.values(),
|
||||
).map((shell, index) => {
|
||||
const truncatedCommand = formatShellCommandForDisplay(
|
||||
shell.command,
|
||||
maxCommandLength,
|
||||
);
|
||||
|
||||
let label = `${index + 1}: ${truncatedCommand} (PID: ${shell.pid})`;
|
||||
if (shell.status === 'exited') {
|
||||
label += ` (Exit Code: ${shell.exitCode})`;
|
||||
}
|
||||
|
||||
return {
|
||||
key: shell.pid.toString(),
|
||||
value: shell.pid,
|
||||
label,
|
||||
};
|
||||
});
|
||||
|
||||
const initialIndex = items.findIndex((item) => item.value === activePid);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" height="100%" width="100%">
|
||||
<Box flexShrink={0} marginBottom={1} paddingTop={1}>
|
||||
<Text bold>
|
||||
{`Select Process (${commandDescriptions[Command.BACKGROUND_SHELL_SELECT]} to select, ${commandDescriptions[Command.BACKGROUND_SHELL_ESCAPE]} to cancel):`}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width="100%">
|
||||
<RadioButtonSelect
|
||||
items={items}
|
||||
initialIndex={initialIndex >= 0 ? initialIndex : 0}
|
||||
onSelect={(pid) => {
|
||||
setActiveBackgroundShellPid(pid);
|
||||
setIsBackgroundShellListOpen(false);
|
||||
}}
|
||||
onHighlight={(pid) => setHighlightedPid(pid)}
|
||||
isFocused={isFocused}
|
||||
maxItemsToShow={Math.max(1, height - HEADER_HEIGHT - 3)} // Adjust for header
|
||||
renderItem={(
|
||||
item,
|
||||
{ isSelected: _isSelected, titleColor: _titleColor },
|
||||
) => {
|
||||
// Custom render to handle exit code coloring if needed,
|
||||
// or just use default. The default RadioButtonSelect renderer
|
||||
// handles standard label.
|
||||
// But we want to color exit code differently?
|
||||
// The previous implementation colored exit code green/red.
|
||||
// Let's reimplement that.
|
||||
|
||||
// We need access to shell details here.
|
||||
// We can put shell details in the item or lookup.
|
||||
// Lookup from shells map.
|
||||
const shell = shells.get(item.value);
|
||||
if (!shell) return <Text>{item.label}</Text>;
|
||||
|
||||
const truncatedCommand = formatShellCommandForDisplay(
|
||||
shell.command,
|
||||
maxCommandLength,
|
||||
);
|
||||
|
||||
return (
|
||||
<Text>
|
||||
{truncatedCommand} (PID: {shell.pid})
|
||||
{shell.status === 'exited' ? (
|
||||
<Text
|
||||
color={
|
||||
shell.exitCode === 0
|
||||
? theme.status.success
|
||||
: theme.status.error
|
||||
}
|
||||
>
|
||||
{' '}
|
||||
(Exit Code: {shell.exitCode})
|
||||
</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOutput = () => {
|
||||
const lines = typeof output === 'string' ? output.split('\n') : output;
|
||||
|
||||
return (
|
||||
<ScrollableList
|
||||
ref={outputRef}
|
||||
data={lines}
|
||||
renderItem={({ item: line, index }) => {
|
||||
if (typeof line === 'string') {
|
||||
return <Text key={index}>{line}</Text>;
|
||||
}
|
||||
return (
|
||||
<Text key={index} wrap="truncate">
|
||||
{line.length > 0
|
||||
? line.map((token: AnsiToken, tokenIndex: number) => (
|
||||
<Text
|
||||
key={tokenIndex}
|
||||
color={token.fg}
|
||||
backgroundColor={token.bg}
|
||||
inverse={token.inverse}
|
||||
dimColor={token.dim}
|
||||
bold={token.bold}
|
||||
italic={token.italic}
|
||||
underline={token.underline}
|
||||
>
|
||||
{token.text}
|
||||
</Text>
|
||||
))
|
||||
: null}
|
||||
</Text>
|
||||
);
|
||||
}}
|
||||
estimatedItemHeight={() => 1}
|
||||
keyExtractor={(_, index) => index.toString()}
|
||||
hasFocus={isFocused}
|
||||
initialScrollIndex={SCROLL_TO_ITEM_END}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
height="100%"
|
||||
width="100%"
|
||||
borderStyle="single"
|
||||
borderColor={isFocused ? theme.border.focused : undefined}
|
||||
>
|
||||
<Box
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
borderStyle="single"
|
||||
borderBottom={false}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderTop={false}
|
||||
paddingX={1}
|
||||
borderColor={isFocused ? theme.border.focused : undefined}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
{renderTabs()}
|
||||
<Text bold>
|
||||
{' '}
|
||||
(PID: {activeShell?.pid}) {isFocused ? '(Focused)' : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text color={theme.text.accent}>{helpText}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} overflow="hidden" paddingX={CONTENT_PADDING_X}>
|
||||
{isListOpenProp ? renderProcessList() : renderOutput()}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -34,6 +34,8 @@ describe('Key Bubbling Regression', () => {
|
||||
questions={choiceQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
availableHeight={20}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Checklist } from './Checklist.js';
|
||||
import type { ChecklistItemData } from './ChecklistItem.js';
|
||||
|
||||
describe('<Checklist />', () => {
|
||||
const items: ChecklistItemData[] = [
|
||||
{ status: 'completed', label: 'Task 1' },
|
||||
{ status: 'in_progress', label: 'Task 2' },
|
||||
{ status: 'pending', label: 'Task 3' },
|
||||
{ status: 'cancelled', label: 'Task 4' },
|
||||
];
|
||||
|
||||
it('renders nothing when list is empty', () => {
|
||||
const { lastFrame } = render(
|
||||
<Checklist title="Test List" items={[]} isExpanded={true} />,
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('renders nothing when collapsed and no active items', () => {
|
||||
const inactiveItems: ChecklistItemData[] = [
|
||||
{ status: 'completed', label: 'Task 1' },
|
||||
{ status: 'cancelled', label: 'Task 2' },
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
<Checklist title="Test List" items={inactiveItems} isExpanded={false} />,
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('renders summary view correctly (collapsed)', () => {
|
||||
const { lastFrame } = render(
|
||||
<Checklist
|
||||
title="Test List"
|
||||
items={items}
|
||||
isExpanded={false}
|
||||
toggleHint="toggle me"
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders expanded view correctly', () => {
|
||||
const { lastFrame } = render(
|
||||
<Checklist
|
||||
title="Test List"
|
||||
items={items}
|
||||
isExpanded={true}
|
||||
toggleHint="toggle me"
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders summary view without in-progress item if none exists', () => {
|
||||
const pendingItems: ChecklistItemData[] = [
|
||||
{ status: 'completed', label: 'Task 1' },
|
||||
{ status: 'pending', label: 'Task 2' },
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
<Checklist title="Test List" items={pendingItems} isExpanded={false} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useMemo } from 'react';
|
||||
import { ChecklistItem, type ChecklistItemData } from './ChecklistItem.js';
|
||||
|
||||
export interface ChecklistProps {
|
||||
title: string;
|
||||
items: ChecklistItemData[];
|
||||
isExpanded: boolean;
|
||||
toggleHint?: string;
|
||||
}
|
||||
|
||||
const ChecklistTitleDisplay: React.FC<{
|
||||
title: string;
|
||||
items: ChecklistItemData[];
|
||||
toggleHint?: string;
|
||||
}> = ({ title, items, toggleHint }) => {
|
||||
const score = useMemo(() => {
|
||||
let total = 0;
|
||||
let completed = 0;
|
||||
for (const item of items) {
|
||||
if (item.status !== 'cancelled') {
|
||||
total += 1;
|
||||
if (item.status === 'completed') {
|
||||
completed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return `${completed}/${total} completed`;
|
||||
}, [items]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" columnGap={2} height={1}>
|
||||
<Text color={theme.text.primary} bold aria-label={`${title} list`}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
{score}
|
||||
{toggleHint ? ` (${toggleHint})` : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ChecklistListDisplay: React.FC<{ items: ChecklistItemData[] }> = ({
|
||||
items,
|
||||
}) => (
|
||||
<Box flexDirection="column" aria-role="list">
|
||||
{items.map((item, index) => (
|
||||
<ChecklistItem
|
||||
item={item}
|
||||
key={`${index}-${item.label}`}
|
||||
role="listitem"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Checklist: React.FC<ChecklistProps> = ({
|
||||
title,
|
||||
items,
|
||||
isExpanded,
|
||||
toggleHint,
|
||||
}) => {
|
||||
const inProgress: ChecklistItemData | null = useMemo(
|
||||
() => items.find((item) => item.status === 'in_progress') || null,
|
||||
[items],
|
||||
);
|
||||
|
||||
const hasActiveItems = useMemo(
|
||||
() =>
|
||||
items.some(
|
||||
(item) => item.status === 'pending' || item.status === 'in_progress',
|
||||
),
|
||||
[items],
|
||||
);
|
||||
|
||||
if (items.length === 0 || (!isExpanded && !hasActiveItems)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderBottom={false}
|
||||
borderRight={false}
|
||||
borderLeft={false}
|
||||
borderColor={theme.border.default}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<Box flexDirection="column" rowGap={1}>
|
||||
<ChecklistTitleDisplay
|
||||
title={title}
|
||||
items={items}
|
||||
toggleHint={toggleHint}
|
||||
/>
|
||||
<ChecklistListDisplay items={items} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box flexDirection="row" columnGap={1} height={1}>
|
||||
<Box flexShrink={0} flexGrow={0}>
|
||||
<ChecklistTitleDisplay
|
||||
title={title}
|
||||
items={items}
|
||||
toggleHint={toggleHint}
|
||||
/>
|
||||
</Box>
|
||||
{inProgress && (
|
||||
<Box flexShrink={1} flexGrow={1}>
|
||||
<ChecklistItem item={inProgress} wrap="truncate" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ChecklistItem, type ChecklistItemData } from './ChecklistItem.js';
|
||||
import { Box } from 'ink';
|
||||
|
||||
describe('<ChecklistItem />', () => {
|
||||
it.each([
|
||||
{ status: 'pending', label: 'Do this' },
|
||||
{ status: 'in_progress', label: 'Doing this' },
|
||||
{ status: 'completed', label: 'Done this' },
|
||||
{ status: 'cancelled', label: 'Skipped this' },
|
||||
] as ChecklistItemData[])('renders %s item correctly', (item) => {
|
||||
const { lastFrame } = render(<ChecklistItem item={item} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('truncates long text when wrap="truncate"', () => {
|
||||
const item: ChecklistItemData = {
|
||||
status: 'in_progress',
|
||||
label:
|
||||
'This is a very long text that should be truncated because the wrap prop is set to truncate',
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<Box width={30}>
|
||||
<ChecklistItem item={item} wrap="truncate" />
|
||||
</Box>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('wraps long text by default', () => {
|
||||
const item: ChecklistItemData = {
|
||||
status: 'in_progress',
|
||||
label:
|
||||
'This is a very long text that should wrap because the default behavior is wrapping',
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<Box width={30}>
|
||||
<ChecklistItem item={item} />
|
||||
</Box>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user