mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-30 11:41:00 -07:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fcff356a8b | |||
| 0213621541 | |||
| 8b01d78512 | |||
| 56c2397e78 | |||
| d1a062bcc4 | |||
| a39461718c | |||
| 3df99d8bcb | |||
| a837b39f8d |
@@ -2,7 +2,6 @@
|
||||
"experimental": {
|
||||
"extensionReloading": true,
|
||||
"modelSteering": true,
|
||||
"memoryManager": false,
|
||||
"topicUpdateNarration": true
|
||||
},
|
||||
"general": {
|
||||
|
||||
@@ -64,3 +64,6 @@ gemini-debug.log
|
||||
evals/logs/
|
||||
|
||||
temp_agents/
|
||||
|
||||
# conductor extension and planning directories
|
||||
conductor/
|
||||
|
||||
+248
-406
@@ -1,6 +1,6 @@
|
||||
# Preview release: v0.37.0-preview.2
|
||||
# Preview release: v0.38.0-preview.0
|
||||
|
||||
Released: April 07, 2026
|
||||
Released: April 08, 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,414 +13,256 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Plan Mode Enhancements**: Plan now includes support for untrusted folders,
|
||||
prioritized pre-approval discussions, and a resolve for sandbox-related
|
||||
deadlocks during file creation.
|
||||
- **Browser Agent Evolved**: Significant updates to the browser agent, including
|
||||
persistent session management, dynamic discovery of read-only tools,
|
||||
sandbox-aware initialization, and automated reclamation of stale snapshots to
|
||||
optimize context window usage.
|
||||
- **Advanced Sandbox Security**: Implementation of dynamic sandbox expansion for
|
||||
both Linux and Windows, alongside secret visibility lockdown for environment
|
||||
files and OS-specific forbidden path support.
|
||||
- **Unified Core Architecture**: Centralized context management and a new
|
||||
`ModelConfigService` for unified model discovery, complemented by the
|
||||
introduction of `AgentHistoryProvider` and tool-based topic grouping
|
||||
(Chapters).
|
||||
- **UI/UX & Performance Improvements**: New Tokyo Night theme, "tab to queue"
|
||||
message support, and compact tool output formatting, plus optimized build
|
||||
scripts and improved layout stability for TUI components.
|
||||
- **Context Management:** Introduced a Context Compression Service to optimize
|
||||
context window usage and landed a background memory service for skill
|
||||
extraction.
|
||||
- **Enhanced Security:** Implemented context-aware persistent policy approvals
|
||||
for smarter tool permissions and enabled `web_fetch` in plan mode with user
|
||||
confirmation.
|
||||
- **Workflow Monitoring:** Added background process monitoring and inspection
|
||||
tools for better visibility into long-running tasks.
|
||||
- **UI/UX Refinements:** Enhanced the tool confirmation UI, selection layout,
|
||||
and added support for selective topic expansion and click-to-expand.
|
||||
- **Core Stability:** Improved sandbox reliability on Linux and Windows,
|
||||
resolved shebang compatibility issues, and fixed various crashes in the CLI
|
||||
and core services.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix(patch): cherry-pick cb7f7d6 to release/v0.37.0-preview.1-pr-24342 to patch
|
||||
version v0.37.0-preview.1 and create version 0.37.0-preview.2 by
|
||||
@gemini-cli-robot in
|
||||
[#24842](https://github.com/google-gemini/gemini-cli/pull/24842)
|
||||
- fix(patch): cherry-pick 64c928f to release/v0.37.0-preview.0-pr-23257 to patch
|
||||
version v0.37.0-preview.0 and create version 0.37.0-preview.1 by
|
||||
@gemini-cli-robot in
|
||||
[#24561](https://github.com/google-gemini/gemini-cli/pull/24561)
|
||||
- feat(evals): centralize test agents into test-utils for reuse by @Samee24 in
|
||||
[#23616](https://github.com/google-gemini/gemini-cli/pull/23616)
|
||||
- revert: chore(config): disable agents by default by @abhipatel12 in
|
||||
[#23672](https://github.com/google-gemini/gemini-cli/pull/23672)
|
||||
- fix(plan): update telemetry attribute keys and add timestamp by @Adib234 in
|
||||
[#23685](https://github.com/google-gemini/gemini-cli/pull/23685)
|
||||
- fix(core): prevent premature MCP discovery completion by @jackwotherspoon in
|
||||
[#23637](https://github.com/google-gemini/gemini-cli/pull/23637)
|
||||
- feat(browser): add maxActionsPerTask for browser agent setting by
|
||||
@cynthialong0-0 in
|
||||
[#23216](https://github.com/google-gemini/gemini-cli/pull/23216)
|
||||
- fix(core): improve agent loader error formatting for empty paths by
|
||||
@adamfweidman in
|
||||
[#23690](https://github.com/google-gemini/gemini-cli/pull/23690)
|
||||
- fix(cli): only show updating spinner when auto-update is in progress by
|
||||
@scidomino in [#23709](https://github.com/google-gemini/gemini-cli/pull/23709)
|
||||
- Refine onboarding metrics to log the duration explicitly and use the tier
|
||||
name. by @yunaseoul in
|
||||
[#23678](https://github.com/google-gemini/gemini-cli/pull/23678)
|
||||
- chore(tools): add toJSON to tools and invocations to reduce logging verbosity
|
||||
by @alisa-alisa in
|
||||
[#22899](https://github.com/google-gemini/gemini-cli/pull/22899)
|
||||
- fix(cli): stabilize copy mode to prevent flickering and cursor resets by
|
||||
@mattKorwel in
|
||||
[#22584](https://github.com/google-gemini/gemini-cli/pull/22584)
|
||||
- fix(test): move flaky ctrl-c-exit test to non-blocking suite by @mattKorwel in
|
||||
[#23732](https://github.com/google-gemini/gemini-cli/pull/23732)
|
||||
- feat(skills): add ci skill for automated failure replication by @mattKorwel in
|
||||
[#23720](https://github.com/google-gemini/gemini-cli/pull/23720)
|
||||
- feat(sandbox): implement forbiddenPaths for OS-specific sandbox managers by
|
||||
@ehedlund in [#23282](https://github.com/google-gemini/gemini-cli/pull/23282)
|
||||
- fix(core): conditionally expose additional_permissions in shell tool by
|
||||
@galz10 in [#23729](https://github.com/google-gemini/gemini-cli/pull/23729)
|
||||
- refactor(core): standardize OS-specific sandbox tests and extract linux helper
|
||||
methods by @ehedlund in
|
||||
[#23715](https://github.com/google-gemini/gemini-cli/pull/23715)
|
||||
- format recently added script by @scidomino in
|
||||
[#23739](https://github.com/google-gemini/gemini-cli/pull/23739)
|
||||
- fix(ui): prevent over-eager slash subcommand completion by @keithguerin in
|
||||
[#20136](https://github.com/google-gemini/gemini-cli/pull/20136)
|
||||
- Fix dynamic model routing for gemini 3.1 pro to customtools model by
|
||||
@kevinjwang1 in
|
||||
[#23641](https://github.com/google-gemini/gemini-cli/pull/23641)
|
||||
- feat(core): support inline agentCardJson for remote agents by @adamfweidman in
|
||||
[#23743](https://github.com/google-gemini/gemini-cli/pull/23743)
|
||||
- fix(cli): skip console log/info in headless mode by @cynthialong0-0 in
|
||||
[#22739](https://github.com/google-gemini/gemini-cli/pull/22739)
|
||||
- test(core): install bubblewrap on Linux CI for sandbox integration tests by
|
||||
@ehedlund in [#23583](https://github.com/google-gemini/gemini-cli/pull/23583)
|
||||
- docs(reference): split tools table into category sections by @sheikhlimon in
|
||||
[#21516](https://github.com/google-gemini/gemini-cli/pull/21516)
|
||||
- fix(browser): detect embedded URLs in query params to prevent allowedDomains
|
||||
bypass by @tony-shi in
|
||||
[#23225](https://github.com/google-gemini/gemini-cli/pull/23225)
|
||||
- fix(browser): add proxy bypass constraint to domain restriction system prompt
|
||||
by @tony-shi in
|
||||
[#23229](https://github.com/google-gemini/gemini-cli/pull/23229)
|
||||
- fix(policy): relax write_file argsPattern in plan mode to allow paths without
|
||||
session ID by @Adib234 in
|
||||
[#23695](https://github.com/google-gemini/gemini-cli/pull/23695)
|
||||
- docs: fix grammar in CONTRIBUTING and numbering in sandbox docs by
|
||||
@splint-disk-8i in
|
||||
[#23448](https://github.com/google-gemini/gemini-cli/pull/23448)
|
||||
- fix(acp): allow attachments by adding a permission prompt by @sripasg in
|
||||
[#23680](https://github.com/google-gemini/gemini-cli/pull/23680)
|
||||
- fix(core): thread AbortSignal to chat compression requests (#20405) by
|
||||
@SH20RAJ in [#20778](https://github.com/google-gemini/gemini-cli/pull/20778)
|
||||
- feat(core): implement Windows sandbox dynamic expansion Phase 1 and 2.1 by
|
||||
@scidomino in [#23691](https://github.com/google-gemini/gemini-cli/pull/23691)
|
||||
- Add note about root privileges in sandbox docs by @diodesign in
|
||||
[#23314](https://github.com/google-gemini/gemini-cli/pull/23314)
|
||||
- docs(core): document agent_card_json string literal options for remote agents
|
||||
by @adamfweidman in
|
||||
[#23797](https://github.com/google-gemini/gemini-cli/pull/23797)
|
||||
- fix(cli): resolve TTY hang on headless environments by unconditionally
|
||||
resuming process.stdin before React Ink launch by @cocosheng-g in
|
||||
[#23673](https://github.com/google-gemini/gemini-cli/pull/23673)
|
||||
- fix(ui): cleanup estimated string length hacks in composer by @keithguerin in
|
||||
[#23694](https://github.com/google-gemini/gemini-cli/pull/23694)
|
||||
- feat(browser): dynamically discover read-only tools by @cynthialong0-0 in
|
||||
[#23805](https://github.com/google-gemini/gemini-cli/pull/23805)
|
||||
- docs: clarify policy requirement for `general.plan.directory` in settings
|
||||
schema by @jerop in
|
||||
[#23784](https://github.com/google-gemini/gemini-cli/pull/23784)
|
||||
- Revert "perf(cli): optimize --version startup time (#23671)" by @scidomino in
|
||||
[#23812](https://github.com/google-gemini/gemini-cli/pull/23812)
|
||||
- don't silence errors from wombat by @scidomino in
|
||||
[#23822](https://github.com/google-gemini/gemini-cli/pull/23822)
|
||||
- fix(ui): prevent escape key from cancelling requests in shell mode by
|
||||
@PrasannaPal21 in
|
||||
[#21245](https://github.com/google-gemini/gemini-cli/pull/21245)
|
||||
- Changelog for v0.36.0-preview.0 by @gemini-cli-robot in
|
||||
[#23702](https://github.com/google-gemini/gemini-cli/pull/23702)
|
||||
- feat(core,ui): Add experiment-gated support for gemini flash 3.1 lite by
|
||||
@chrstnb in [#23794](https://github.com/google-gemini/gemini-cli/pull/23794)
|
||||
- Changelog for v0.36.0-preview.3 by @gemini-cli-robot in
|
||||
[#23827](https://github.com/google-gemini/gemini-cli/pull/23827)
|
||||
- new linting check: github-actions-pinning by @alisa-alisa in
|
||||
[#23808](https://github.com/google-gemini/gemini-cli/pull/23808)
|
||||
- fix(cli): show helpful guidance when no skills are available by @Niralisj in
|
||||
[#23785](https://github.com/google-gemini/gemini-cli/pull/23785)
|
||||
- fix: Chat logs and errors handle tail tool calls correctly by @googlestrobe in
|
||||
[#22460](https://github.com/google-gemini/gemini-cli/pull/22460)
|
||||
- Don't try removing a tag from a non-existent release. by @scidomino in
|
||||
[#23830](https://github.com/google-gemini/gemini-cli/pull/23830)
|
||||
- fix(cli): allow ask question dialog to take full window height by @jacob314 in
|
||||
[#23693](https://github.com/google-gemini/gemini-cli/pull/23693)
|
||||
- fix(core): strip leading underscores from error types in telemetry by
|
||||
@yunaseoul in [#23824](https://github.com/google-gemini/gemini-cli/pull/23824)
|
||||
- Changelog for v0.35.0 by @gemini-cli-robot in
|
||||
[#23819](https://github.com/google-gemini/gemini-cli/pull/23819)
|
||||
- feat(evals): add reliability harvester and 500/503 retry support by
|
||||
@alisa-alisa in
|
||||
[#23626](https://github.com/google-gemini/gemini-cli/pull/23626)
|
||||
- feat(sandbox): dynamic Linux sandbox expansion and worktree support by @galz10
|
||||
in [#23692](https://github.com/google-gemini/gemini-cli/pull/23692)
|
||||
- Merge examples of use into quickstart documentation by @diodesign in
|
||||
[#23319](https://github.com/google-gemini/gemini-cli/pull/23319)
|
||||
- fix(cli): prioritize primary name matches in slash command search by @sehoon38
|
||||
in [#23850](https://github.com/google-gemini/gemini-cli/pull/23850)
|
||||
- Changelog for v0.35.1 by @gemini-cli-robot in
|
||||
[#23840](https://github.com/google-gemini/gemini-cli/pull/23840)
|
||||
- fix(browser): keep input blocker active across navigations by @kunal-10-cloud
|
||||
in [#22562](https://github.com/google-gemini/gemini-cli/pull/22562)
|
||||
- feat(core): new skill to look for duplicated code while reviewing PRs by
|
||||
@devr0306 in [#23704](https://github.com/google-gemini/gemini-cli/pull/23704)
|
||||
- fix(core): replace hardcoded non-interactive ASK_USER denial with explicit
|
||||
policy rules by @ruomengz in
|
||||
[#23668](https://github.com/google-gemini/gemini-cli/pull/23668)
|
||||
- fix(plan): after exiting plan mode switches model to a flash model by @Adib234
|
||||
in [#23885](https://github.com/google-gemini/gemini-cli/pull/23885)
|
||||
- feat(gcp): add development worker infrastructure by @mattKorwel in
|
||||
[#23814](https://github.com/google-gemini/gemini-cli/pull/23814)
|
||||
- fix(a2a-server): A2A server should execute ask policies in interactive mode by
|
||||
@kschaab in [#23831](https://github.com/google-gemini/gemini-cli/pull/23831)
|
||||
- feat(core): define TrajectoryProvider interface by @sehoon38 in
|
||||
[#23050](https://github.com/google-gemini/gemini-cli/pull/23050)
|
||||
- Docs: Update quotas and pricing by @jkcinouye in
|
||||
[#23835](https://github.com/google-gemini/gemini-cli/pull/23835)
|
||||
- fix(core): allow disabling environment variable redaction by @galz10 in
|
||||
[#23927](https://github.com/google-gemini/gemini-cli/pull/23927)
|
||||
- feat(cli): enable notifications cross-platform via terminal bell fallback by
|
||||
@genneth in [#21618](https://github.com/google-gemini/gemini-cli/pull/21618)
|
||||
- feat(sandbox): implement secret visibility lockdown for env files by
|
||||
@DavidAPierce in
|
||||
[#23712](https://github.com/google-gemini/gemini-cli/pull/23712)
|
||||
- fix(core): remove shell outputChunks buffer caching to prevent memory bloat
|
||||
and sanitize prompt input by @spencer426 in
|
||||
[#23751](https://github.com/google-gemini/gemini-cli/pull/23751)
|
||||
- feat(core): implement persistent browser session management by @kunal-10-cloud
|
||||
in [#21306](https://github.com/google-gemini/gemini-cli/pull/21306)
|
||||
- refactor(core): delegate sandbox denial parsing to SandboxManager by
|
||||
@scidomino in [#23928](https://github.com/google-gemini/gemini-cli/pull/23928)
|
||||
- dep(update) Update Ink version to 6.5.0 by @jacob314 in
|
||||
[#23843](https://github.com/google-gemini/gemini-cli/pull/23843)
|
||||
- Docs: Update 'docs-writer' skill for relative links by @jkcinouye in
|
||||
[#21463](https://github.com/google-gemini/gemini-cli/pull/21463)
|
||||
- Changelog for v0.36.0-preview.4 by @gemini-cli-robot in
|
||||
[#23935](https://github.com/google-gemini/gemini-cli/pull/23935)
|
||||
- fix(acp): Update allow approval policy flow for ACP clients to fix config
|
||||
persistence and compatible with TUI by @sripasg in
|
||||
[#23818](https://github.com/google-gemini/gemini-cli/pull/23818)
|
||||
- Changelog for v0.35.2 by @gemini-cli-robot in
|
||||
[#23960](https://github.com/google-gemini/gemini-cli/pull/23960)
|
||||
- ACP integration documents by @g-samroberts in
|
||||
[#22254](https://github.com/google-gemini/gemini-cli/pull/22254)
|
||||
- fix(core): explicitly set error names to avoid bundling renaming issues by
|
||||
@yunaseoul in [#23913](https://github.com/google-gemini/gemini-cli/pull/23913)
|
||||
- feat(core): subagent isolation and cleanup hardening by @abhipatel12 in
|
||||
[#23903](https://github.com/google-gemini/gemini-cli/pull/23903)
|
||||
- disable extension-reload test by @scidomino in
|
||||
[#24018](https://github.com/google-gemini/gemini-cli/pull/24018)
|
||||
- feat(core): add forbiddenPaths to GlobalSandboxOptions and refactor
|
||||
createSandboxManager by @ehedlund in
|
||||
[#23936](https://github.com/google-gemini/gemini-cli/pull/23936)
|
||||
- refactor(core): improve ignore resolution and fix directory-matching bug by
|
||||
@ehedlund in [#23816](https://github.com/google-gemini/gemini-cli/pull/23816)
|
||||
- revert(core): support custom base URL via env vars by @spencer426 in
|
||||
[#23976](https://github.com/google-gemini/gemini-cli/pull/23976)
|
||||
- Increase memory limited for eslint. by @jacob314 in
|
||||
[#24022](https://github.com/google-gemini/gemini-cli/pull/24022)
|
||||
- fix(acp): prevent crash on empty response in ACP mode by @sripasg in
|
||||
[#23952](https://github.com/google-gemini/gemini-cli/pull/23952)
|
||||
- feat(core): Land `AgentHistoryProvider`. by @joshualitt in
|
||||
[#23978](https://github.com/google-gemini/gemini-cli/pull/23978)
|
||||
- fix(core): switch to subshells for shell tool wrapping to fix heredocs and
|
||||
edge cases by @abhipatel12 in
|
||||
[#24024](https://github.com/google-gemini/gemini-cli/pull/24024)
|
||||
- Debug command. by @jacob314 in
|
||||
[#23851](https://github.com/google-gemini/gemini-cli/pull/23851)
|
||||
- Changelog for v0.36.0-preview.5 by @gemini-cli-robot in
|
||||
[#24046](https://github.com/google-gemini/gemini-cli/pull/24046)
|
||||
- Fix test flakes by globally mocking ink-spinner by @jacob314 in
|
||||
[#24044](https://github.com/google-gemini/gemini-cli/pull/24044)
|
||||
- Enable network access in sandbox configuration by @galz10 in
|
||||
[#24055](https://github.com/google-gemini/gemini-cli/pull/24055)
|
||||
- feat(context): add configurable memoryBoundaryMarkers setting by @SandyTao520
|
||||
in [#24020](https://github.com/google-gemini/gemini-cli/pull/24020)
|
||||
- feat(core): implement windows sandbox expansion and denial detection by
|
||||
@scidomino in [#24027](https://github.com/google-gemini/gemini-cli/pull/24027)
|
||||
- fix(core): resolve ACP Operation Aborted Errors in grep_search by @ivanporty
|
||||
in [#23821](https://github.com/google-gemini/gemini-cli/pull/23821)
|
||||
- fix(hooks): prevent SessionEnd from firing twice in non-interactive mode by
|
||||
@krishdef7 in [#22139](https://github.com/google-gemini/gemini-cli/pull/22139)
|
||||
- Re-word intro to Gemini 3 page. by @g-samroberts in
|
||||
[#24069](https://github.com/google-gemini/gemini-cli/pull/24069)
|
||||
- fix(cli): resolve layout contention and flashing loop in StatusRow by
|
||||
@keithguerin in
|
||||
[#24065](https://github.com/google-gemini/gemini-cli/pull/24065)
|
||||
- fix(sandbox): implement Windows Mandatory Integrity Control for GeminiSandbox
|
||||
by @galz10 in [#24057](https://github.com/google-gemini/gemini-cli/pull/24057)
|
||||
- feat(core): implement tool-based topic grouping (Chapters) by @Abhijit-2592 in
|
||||
[#23150](https://github.com/google-gemini/gemini-cli/pull/23150)
|
||||
- feat(cli): support 'tab to queue' for messages while generating by @gundermanc
|
||||
in [#24052](https://github.com/google-gemini/gemini-cli/pull/24052)
|
||||
- feat(core): agnostic background task UI with CompletionBehavior by
|
||||
@adamfweidman in
|
||||
[#22740](https://github.com/google-gemini/gemini-cli/pull/22740)
|
||||
- UX for topic narration tool by @gundermanc in
|
||||
[#24079](https://github.com/google-gemini/gemini-cli/pull/24079)
|
||||
- fix: shellcheck warnings in scripts by @scidomino in
|
||||
[#24035](https://github.com/google-gemini/gemini-cli/pull/24035)
|
||||
- test(evals): add comprehensive subagent delegation evaluations by @abhipatel12
|
||||
in [#24132](https://github.com/google-gemini/gemini-cli/pull/24132)
|
||||
- fix(a2a-server): prioritize ADC before evaluating headless constraints for
|
||||
auth initialization by @spencer426 in
|
||||
[#23614](https://github.com/google-gemini/gemini-cli/pull/23614)
|
||||
- Text can be added after /plan command by @rambleraptor in
|
||||
[#22833](https://github.com/google-gemini/gemini-cli/pull/22833)
|
||||
- fix(cli): resolve missing F12 logs via global console store by @scidomino in
|
||||
[#24235](https://github.com/google-gemini/gemini-cli/pull/24235)
|
||||
- fix broken tests by @scidomino in
|
||||
[#24279](https://github.com/google-gemini/gemini-cli/pull/24279)
|
||||
- fix(evals): add update_topic behavioral eval by @gundermanc in
|
||||
[#24223](https://github.com/google-gemini/gemini-cli/pull/24223)
|
||||
- feat(core): Unified Context Management and Tool Distillation. by @joshualitt
|
||||
in [#24157](https://github.com/google-gemini/gemini-cli/pull/24157)
|
||||
- Default enable narration for the team. by @gundermanc in
|
||||
[#24224](https://github.com/google-gemini/gemini-cli/pull/24224)
|
||||
- fix(core): ensure default agents provide tools and use model-specific schemas
|
||||
by @abhipatel12 in
|
||||
[#24268](https://github.com/google-gemini/gemini-cli/pull/24268)
|
||||
- feat(cli): show Flash Lite Preview model regardless of user tier by @sehoon38
|
||||
in [#23904](https://github.com/google-gemini/gemini-cli/pull/23904)
|
||||
- feat(cli): implement compact tool output by @jwhelangoog in
|
||||
[#20974](https://github.com/google-gemini/gemini-cli/pull/20974)
|
||||
- Add security settings for tool sandboxing by @galz10 in
|
||||
[#23923](https://github.com/google-gemini/gemini-cli/pull/23923)
|
||||
- chore(test-utils): switch integration tests to use PREVIEW_GEMINI_MODEL by
|
||||
@sehoon38 in [#24276](https://github.com/google-gemini/gemini-cli/pull/24276)
|
||||
- feat(core): enable topic update narration for legacy models by @Abhijit-2592
|
||||
in [#24241](https://github.com/google-gemini/gemini-cli/pull/24241)
|
||||
- feat(core): add project-level memory scope to save_memory tool by @SandyTao520
|
||||
in [#24161](https://github.com/google-gemini/gemini-cli/pull/24161)
|
||||
- test(integration): fix plan mode write denial test false positive by @sehoon38
|
||||
in [#24299](https://github.com/google-gemini/gemini-cli/pull/24299)
|
||||
- feat(plan): support `Plan` mode in untrusted folders by @Adib234 in
|
||||
[#17586](https://github.com/google-gemini/gemini-cli/pull/17586)
|
||||
- fix(core): enable mid-stream retries for all models and re-enable compression
|
||||
test by @sehoon38 in
|
||||
[#24302](https://github.com/google-gemini/gemini-cli/pull/24302)
|
||||
- Changelog for v0.36.0-preview.6 by @gemini-cli-robot in
|
||||
[#24082](https://github.com/google-gemini/gemini-cli/pull/24082)
|
||||
- Changelog for v0.35.3 by @gemini-cli-robot in
|
||||
[#24083](https://github.com/google-gemini/gemini-cli/pull/24083)
|
||||
- feat(cli): add auth info to footer by @sehoon38 in
|
||||
[#24042](https://github.com/google-gemini/gemini-cli/pull/24042)
|
||||
- fix(browser): reset action counter for each agent session and let it ignore
|
||||
internal actions by @cynthialong0-0 in
|
||||
[#24228](https://github.com/google-gemini/gemini-cli/pull/24228)
|
||||
- feat(plan): promote planning feature to stable by @ruomengz in
|
||||
[#24282](https://github.com/google-gemini/gemini-cli/pull/24282)
|
||||
- fix(browser): terminate subagent immediately on domain restriction violations
|
||||
by @gsquared94 in
|
||||
[#24313](https://github.com/google-gemini/gemini-cli/pull/24313)
|
||||
- feat(cli): add UI to update extensions by @ruomengz in
|
||||
[#23682](https://github.com/google-gemini/gemini-cli/pull/23682)
|
||||
- Fix(browser): terminate immediately for "browser is already running" error by
|
||||
@cynthialong0-0 in
|
||||
[#24233](https://github.com/google-gemini/gemini-cli/pull/24233)
|
||||
- docs: Add 'plan' option to approval mode in CLI reference by @YifanRuan in
|
||||
[#24134](https://github.com/google-gemini/gemini-cli/pull/24134)
|
||||
- fix(core): batch macOS seatbelt rules into a profile file to prevent ARG_MAX
|
||||
errors by @ehedlund in
|
||||
[#24255](https://github.com/google-gemini/gemini-cli/pull/24255)
|
||||
- fix(core): fix race condition between browser agent and main closing process
|
||||
by @cynthialong0-0 in
|
||||
[#24340](https://github.com/google-gemini/gemini-cli/pull/24340)
|
||||
- perf(build): optimize build scripts for parallel execution and remove
|
||||
redundant checks by @sehoon38 in
|
||||
[#24307](https://github.com/google-gemini/gemini-cli/pull/24307)
|
||||
- ci: install bubblewrap on Linux for release workflows by @ehedlund in
|
||||
[#24347](https://github.com/google-gemini/gemini-cli/pull/24347)
|
||||
- chore(release): allow bundling for all builds, including stable by @sehoon38
|
||||
in [#24305](https://github.com/google-gemini/gemini-cli/pull/24305)
|
||||
- Revert "Add security settings for tool sandboxing" by @jerop in
|
||||
[#24357](https://github.com/google-gemini/gemini-cli/pull/24357)
|
||||
- docs: update subagents docs to not be experimental by @abhipatel12 in
|
||||
[#24343](https://github.com/google-gemini/gemini-cli/pull/24343)
|
||||
- fix(core): implement **read and **write commands in sandbox managers by
|
||||
@galz10 in [#24283](https://github.com/google-gemini/gemini-cli/pull/24283)
|
||||
- don't try to remove tags in dry run by @scidomino in
|
||||
[#24356](https://github.com/google-gemini/gemini-cli/pull/24356)
|
||||
- fix(config): disable JIT context loading by default by @SandyTao520 in
|
||||
[#24364](https://github.com/google-gemini/gemini-cli/pull/24364)
|
||||
- test(sandbox): add integration test for dynamic permission expansion by
|
||||
@galz10 in [#24359](https://github.com/google-gemini/gemini-cli/pull/24359)
|
||||
- docs(policy): remove unsupported mcpName wildcard edge case by @abhipatel12 in
|
||||
[#24133](https://github.com/google-gemini/gemini-cli/pull/24133)
|
||||
- docs: fix broken GEMINI.md link in CONTRIBUTING.md by @Panchal-Tirth in
|
||||
[#24182](https://github.com/google-gemini/gemini-cli/pull/24182)
|
||||
- feat(core): infrastructure for event-driven subagent history by @abhipatel12
|
||||
in [#23914](https://github.com/google-gemini/gemini-cli/pull/23914)
|
||||
- fix(core): resolve Plan Mode deadlock during plan file creation due to sandbox
|
||||
restrictions by @DavidAPierce in
|
||||
[#24047](https://github.com/google-gemini/gemini-cli/pull/24047)
|
||||
- fix(core): fix browser agent UX issues and improve E2E test reliability by
|
||||
@gsquared94 in
|
||||
[#24312](https://github.com/google-gemini/gemini-cli/pull/24312)
|
||||
- fix(ui): wrap topic and intent fields in TopicMessage by @jwhelangoog in
|
||||
[#24386](https://github.com/google-gemini/gemini-cli/pull/24386)
|
||||
- refactor(core): Centralize context management logic into src/context by
|
||||
@joshualitt in
|
||||
[#24380](https://github.com/google-gemini/gemini-cli/pull/24380)
|
||||
- fix(core): pin AuthType.GATEWAY to use Gemini 3.1 Pro/Flash Lite by default by
|
||||
@sripasg in [#24375](https://github.com/google-gemini/gemini-cli/pull/24375)
|
||||
- feat(ui): add Tokyo Night theme by @danrneal in
|
||||
[#24054](https://github.com/google-gemini/gemini-cli/pull/24054)
|
||||
- fix(cli): refactor test config loading and mock debugLogger in test-setup by
|
||||
@mattKorwel in
|
||||
[#24389](https://github.com/google-gemini/gemini-cli/pull/24389)
|
||||
- Set memoryManager to false in settings.json by @mattKorwel in
|
||||
[#24393](https://github.com/google-gemini/gemini-cli/pull/24393)
|
||||
- ink 6.6.3 by @jacob314 in
|
||||
[#24372](https://github.com/google-gemini/gemini-cli/pull/24372)
|
||||
- fix(core): resolve subagent chat recording gaps and directory inheritance by
|
||||
- fix(cli): refresh slash command list after /skills reload by @NTaylorMullen in
|
||||
[#24454](https://github.com/google-gemini/gemini-cli/pull/24454)
|
||||
- Update README.md for links. by @g-samroberts in
|
||||
[#22759](https://github.com/google-gemini/gemini-cli/pull/22759)
|
||||
- fix(core): ensure complete_task tool calls are recorded in chat history by
|
||||
@abhipatel12 in
|
||||
[#24368](https://github.com/google-gemini/gemini-cli/pull/24368)
|
||||
- fix(cli): cap shell output at 10 MB to prevent RangeError crash by @ProthamD
|
||||
in [#24168](https://github.com/google-gemini/gemini-cli/pull/24168)
|
||||
- feat(plan): conditionally add enter/exit plan mode tools based on current mode
|
||||
by @ruomengz in
|
||||
[#24378](https://github.com/google-gemini/gemini-cli/pull/24378)
|
||||
- feat(core): prioritize discussion before formal plan approval by @jerop in
|
||||
[#24423](https://github.com/google-gemini/gemini-cli/pull/24423)
|
||||
- fix(ui): add accelerated scrolling on alternate buffer mode by @devr0306 in
|
||||
[#23940](https://github.com/google-gemini/gemini-cli/pull/23940)
|
||||
- feat(core): populate sandbox forbidden paths with project ignore file contents
|
||||
by @ehedlund in
|
||||
[#24038](https://github.com/google-gemini/gemini-cli/pull/24038)
|
||||
- fix(core): ensure blue border overlay and input blocker to act correctly
|
||||
depending on browser agent activities by @cynthialong0-0 in
|
||||
[#24385](https://github.com/google-gemini/gemini-cli/pull/24385)
|
||||
- fix(ui): removed additional vertical padding for tables by @devr0306 in
|
||||
[#24381](https://github.com/google-gemini/gemini-cli/pull/24381)
|
||||
- fix(build): upload full bundle directory archive to GitHub releases by
|
||||
@sehoon38 in [#24403](https://github.com/google-gemini/gemini-cli/pull/24403)
|
||||
- fix(build): wire bundle:browser-mcp into bundle pipeline by @gsquared94 in
|
||||
[#24424](https://github.com/google-gemini/gemini-cli/pull/24424)
|
||||
- feat(browser): add sandbox-aware browser agent initialization by @gsquared94
|
||||
in [#24419](https://github.com/google-gemini/gemini-cli/pull/24419)
|
||||
- feat(core): enhance tracker task schemas for detailed titles and descriptions
|
||||
by @anj-s in [#23902](https://github.com/google-gemini/gemini-cli/pull/23902)
|
||||
- refactor(core): Unified context management settings schema by @joshualitt in
|
||||
[#24391](https://github.com/google-gemini/gemini-cli/pull/24391)
|
||||
- feat(core): update browser agent prompt to check open pages first when
|
||||
bringing up by @cynthialong0-0 in
|
||||
[#24431](https://github.com/google-gemini/gemini-cli/pull/24431)
|
||||
- fix(acp) refactor(core,cli): centralize model discovery logic in
|
||||
ModelConfigService by @sripasg in
|
||||
[#24392](https://github.com/google-gemini/gemini-cli/pull/24392)
|
||||
- Changelog for v0.36.0-preview.7 by @gemini-cli-robot in
|
||||
[#24346](https://github.com/google-gemini/gemini-cli/pull/24346)
|
||||
- fix: update task tracker storage location in system prompt by @anj-s in
|
||||
[#24034](https://github.com/google-gemini/gemini-cli/pull/24034)
|
||||
- feat(browser): supersede stale snapshots to reclaim context-window tokens by
|
||||
[#24437](https://github.com/google-gemini/gemini-cli/pull/24437)
|
||||
- feat(policy): explicitly allow web_fetch in plan mode with ask_user by
|
||||
@Adib234 in [#24456](https://github.com/google-gemini/gemini-cli/pull/24456)
|
||||
- fix(core): refactor linux sandbox to fix ARG_MAX crashes by @ehedlund in
|
||||
[#24286](https://github.com/google-gemini/gemini-cli/pull/24286)
|
||||
- feat(config): add experimental.adk.agentSessionNoninteractiveEnabled setting
|
||||
by @adamfweidman in
|
||||
[#24439](https://github.com/google-gemini/gemini-cli/pull/24439)
|
||||
- Changelog for v0.36.0-preview.8 by @gemini-cli-robot in
|
||||
[#24453](https://github.com/google-gemini/gemini-cli/pull/24453)
|
||||
- feat(cli): change default loadingPhrases to 'off' to hide tips by @keithguerin
|
||||
in [#24342](https://github.com/google-gemini/gemini-cli/pull/24342)
|
||||
- fix(cli): ensure agent stops when all declinable tools are cancelled by
|
||||
@NTaylorMullen in
|
||||
[#24479](https://github.com/google-gemini/gemini-cli/pull/24479)
|
||||
- fix(core): enhance sandbox usability and fix build error by @galz10 in
|
||||
[#24460](https://github.com/google-gemini/gemini-cli/pull/24460)
|
||||
- Terminal Serializer Optimization by @jacob314 in
|
||||
[#24485](https://github.com/google-gemini/gemini-cli/pull/24485)
|
||||
- Auto configure memory. by @jacob314 in
|
||||
[#24474](https://github.com/google-gemini/gemini-cli/pull/24474)
|
||||
- Unused error variables in catch block are not allowed by @alisa-alisa in
|
||||
[#24487](https://github.com/google-gemini/gemini-cli/pull/24487)
|
||||
- feat(core): add background memory service for skill extraction by @SandyTao520
|
||||
in [#24274](https://github.com/google-gemini/gemini-cli/pull/24274)
|
||||
- feat: implement high-signal PR regression check for evaluations by
|
||||
@alisa-alisa in
|
||||
[#23937](https://github.com/google-gemini/gemini-cli/pull/23937)
|
||||
- Fix shell output display by @jacob314 in
|
||||
[#24490](https://github.com/google-gemini/gemini-cli/pull/24490)
|
||||
- fix(ui): resolve unwanted vertical spacing around various tool output
|
||||
treatments by @jwhelangoog in
|
||||
[#24449](https://github.com/google-gemini/gemini-cli/pull/24449)
|
||||
- revert(cli): bring back input box and footer visibility in copy mode by
|
||||
@sehoon38 in [#24504](https://github.com/google-gemini/gemini-cli/pull/24504)
|
||||
- fix(cli): prevent crash in AnsiOutputText when handling non-array data by
|
||||
@sehoon38 in [#24498](https://github.com/google-gemini/gemini-cli/pull/24498)
|
||||
- feat(cli): support default values for environment variables by @ruomengz in
|
||||
[#24469](https://github.com/google-gemini/gemini-cli/pull/24469)
|
||||
- Implement background process monitoring and inspection tools by @cocosheng-g
|
||||
in [#23799](https://github.com/google-gemini/gemini-cli/pull/23799)
|
||||
- docs(browser-agent): update stale browser agent documentation by @gsquared94
|
||||
in [#24463](https://github.com/google-gemini/gemini-cli/pull/24463)
|
||||
- fix: enable browser_agent in integration tests and add localhost fixture tests
|
||||
by @gsquared94 in
|
||||
[#24523](https://github.com/google-gemini/gemini-cli/pull/24523)
|
||||
- fix(browser): handle computer-use model detection for analyze_screenshot by
|
||||
@gsquared94 in
|
||||
[#24440](https://github.com/google-gemini/gemini-cli/pull/24440)
|
||||
- docs(core): add subagent tool isolation draft doc by @akh64bit in
|
||||
[#23275](https://github.com/google-gemini/gemini-cli/pull/23275)
|
||||
[#24502](https://github.com/google-gemini/gemini-cli/pull/24502)
|
||||
- feat(core): Land ContextCompressionService by @joshualitt in
|
||||
[#24483](https://github.com/google-gemini/gemini-cli/pull/24483)
|
||||
- feat(core): scope subagent workspace directories via AsyncLocalStorage by
|
||||
@SandyTao520 in
|
||||
[#24445](https://github.com/google-gemini/gemini-cli/pull/24445)
|
||||
- Update ink version to 6.6.7 by @jacob314 in
|
||||
[#24514](https://github.com/google-gemini/gemini-cli/pull/24514)
|
||||
- fix(acp): handle all InvalidStreamError types gracefully in prompt by @sripasg
|
||||
in [#24540](https://github.com/google-gemini/gemini-cli/pull/24540)
|
||||
- Fix crash when vim editor is not found in PATH on Windows by
|
||||
@Nagajyothi-tammisetti in
|
||||
[#22423](https://github.com/google-gemini/gemini-cli/pull/22423)
|
||||
- fix(core): move project memory dir under tmp directory by @SandyTao520 in
|
||||
[#24542](https://github.com/google-gemini/gemini-cli/pull/24542)
|
||||
- Enable 'Other' option for yesno question type by @ruomengz in
|
||||
[#24545](https://github.com/google-gemini/gemini-cli/pull/24545)
|
||||
- fix(cli): clear stale retry/loading state after cancellation (#21096) by
|
||||
@Aaxhirrr in [#21960](https://github.com/google-gemini/gemini-cli/pull/21960)
|
||||
- Changelog for v0.37.0-preview.0 by @gemini-cli-robot in
|
||||
[#24464](https://github.com/google-gemini/gemini-cli/pull/24464)
|
||||
- feat(core): implement context-aware persistent policy approvals by @jerop in
|
||||
[#23257](https://github.com/google-gemini/gemini-cli/pull/23257)
|
||||
- docs: move agent disabling instructions and update remote agent status by
|
||||
@jackwotherspoon in
|
||||
[#24559](https://github.com/google-gemini/gemini-cli/pull/24559)
|
||||
- feat(cli): migrate nonInteractiveCli to LegacyAgentSession by @adamfweidman in
|
||||
[#22987](https://github.com/google-gemini/gemini-cli/pull/22987)
|
||||
- fix(core): unsafe type assertions in Core File System #19712 by
|
||||
@aniketsaurav18 in
|
||||
[#19739](https://github.com/google-gemini/gemini-cli/pull/19739)
|
||||
- fix(ui): hide model quota in /stats and refactor quota display by @danzaharia1
|
||||
in [#24206](https://github.com/google-gemini/gemini-cli/pull/24206)
|
||||
- Changelog for v0.36.0 by @gemini-cli-robot in
|
||||
[#24558](https://github.com/google-gemini/gemini-cli/pull/24558)
|
||||
- Changelog for v0.37.0-preview.1 by @gemini-cli-robot in
|
||||
[#24568](https://github.com/google-gemini/gemini-cli/pull/24568)
|
||||
- docs: add missing .md extensions to internal doc links by @ishaan-arora-1 in
|
||||
[#24145](https://github.com/google-gemini/gemini-cli/pull/24145)
|
||||
- fix(ui): fixed table styling by @devr0306 in
|
||||
[#24565](https://github.com/google-gemini/gemini-cli/pull/24565)
|
||||
- fix(core): pass includeDirectories to sandbox configuration by @galz10 in
|
||||
[#24573](https://github.com/google-gemini/gemini-cli/pull/24573)
|
||||
- feat(ui): enable "TerminalBuffer" mode to solve flicker by @jacob314 in
|
||||
[#24512](https://github.com/google-gemini/gemini-cli/pull/24512)
|
||||
- docs: clarify release coordination by @scidomino in
|
||||
[#24575](https://github.com/google-gemini/gemini-cli/pull/24575)
|
||||
- fix(core): remove broken PowerShell translation and fix native \_\_write in
|
||||
Windows sandbox by @scidomino in
|
||||
[#24571](https://github.com/google-gemini/gemini-cli/pull/24571)
|
||||
- Add instructions for how to start react in prod and force react to prod mode
|
||||
by @jacob314 in
|
||||
[#24590](https://github.com/google-gemini/gemini-cli/pull/24590)
|
||||
- feat(cli): minimalist sandbox status labels by @galz10 in
|
||||
[#24582](https://github.com/google-gemini/gemini-cli/pull/24582)
|
||||
- Feat/browser agent metrics by @kunal-10-cloud in
|
||||
[#24210](https://github.com/google-gemini/gemini-cli/pull/24210)
|
||||
- test: fix Windows CI execution and resolve exposed platform failures by
|
||||
@ehedlund in [#24476](https://github.com/google-gemini/gemini-cli/pull/24476)
|
||||
- feat(core,cli): prioritize summary for topics (#24608) by @Abhijit-2592 in
|
||||
[#24609](https://github.com/google-gemini/gemini-cli/pull/24609)
|
||||
- show color by @jacob314 in
|
||||
[#24613](https://github.com/google-gemini/gemini-cli/pull/24613)
|
||||
- feat(cli): enable compact tool output by default (#24509) by @jwhelangoog in
|
||||
[#24510](https://github.com/google-gemini/gemini-cli/pull/24510)
|
||||
- fix(core): inject skill system instructions into subagent prompts if activated
|
||||
by @abhipatel12 in
|
||||
[#24620](https://github.com/google-gemini/gemini-cli/pull/24620)
|
||||
- fix(core): improve windows sandbox reliability and fix integration tests by
|
||||
@ehedlund in [#24480](https://github.com/google-gemini/gemini-cli/pull/24480)
|
||||
- fix(core): ensure sandbox approvals are correctly persisted and matched for
|
||||
proactive expansions by @galz10 in
|
||||
[#24577](https://github.com/google-gemini/gemini-cli/pull/24577)
|
||||
- feat(cli) Scrollbar for input prompt by @jacob314 in
|
||||
[#21992](https://github.com/google-gemini/gemini-cli/pull/21992)
|
||||
- Do not run pr-eval workflow when no steering changes detected by @alisa-alisa
|
||||
in [#24621](https://github.com/google-gemini/gemini-cli/pull/24621)
|
||||
- Fix restoration of topic headers. by @gundermanc in
|
||||
[#24650](https://github.com/google-gemini/gemini-cli/pull/24650)
|
||||
- feat(core): discourage update topic tool for simple tasks by @Samee24 in
|
||||
[#24640](https://github.com/google-gemini/gemini-cli/pull/24640)
|
||||
- fix(core): ensure global temp directory is always in sandbox allowed paths by
|
||||
@galz10 in [#24638](https://github.com/google-gemini/gemini-cli/pull/24638)
|
||||
- fix(core): detect uninitialized lines by @jacob314 in
|
||||
[#24646](https://github.com/google-gemini/gemini-cli/pull/24646)
|
||||
- docs: update sandboxing documentation and toolSandboxing settings by @galz10
|
||||
in [#24655](https://github.com/google-gemini/gemini-cli/pull/24655)
|
||||
- feat(cli): enhance tool confirmation UI and selection layout by @galz10 in
|
||||
[#24376](https://github.com/google-gemini/gemini-cli/pull/24376)
|
||||
- feat(acp): add support for `/about` command by @sripasg in
|
||||
[#24649](https://github.com/google-gemini/gemini-cli/pull/24649)
|
||||
- feat(cli): add role specific metrics to /stats by @cynthialong0-0 in
|
||||
[#24659](https://github.com/google-gemini/gemini-cli/pull/24659)
|
||||
- split context by @jacob314 in
|
||||
[#24623](https://github.com/google-gemini/gemini-cli/pull/24623)
|
||||
- fix(cli): remove -S from shebang to fix Windows and BSD execution by
|
||||
@scidomino in [#24756](https://github.com/google-gemini/gemini-cli/pull/24756)
|
||||
- Fix issue where topic headers can be posted back to back by @gundermanc in
|
||||
[#24759](https://github.com/google-gemini/gemini-cli/pull/24759)
|
||||
- fix(core): handle partial llm_request in BeforeModel hook override by
|
||||
@krishdef7 in [#22326](https://github.com/google-gemini/gemini-cli/pull/22326)
|
||||
- fix(ui): improve narration suppression and reduce flicker by @gundermanc in
|
||||
[#24635](https://github.com/google-gemini/gemini-cli/pull/24635)
|
||||
- fix(ui): fixed auth race condition causing logo to flicker by @devr0306 in
|
||||
[#24652](https://github.com/google-gemini/gemini-cli/pull/24652)
|
||||
- fix(browser): remove premature browser cleanup after subagent invocation by
|
||||
@gsquared94 in
|
||||
[#24753](https://github.com/google-gemini/gemini-cli/pull/24753)
|
||||
- Revert "feat(core,cli): prioritize summary for topics (#24608)" by
|
||||
@Abhijit-2592 in
|
||||
[#24777](https://github.com/google-gemini/gemini-cli/pull/24777)
|
||||
- relax tool sandboxing overrides for plan mode to match defaults. by
|
||||
@DavidAPierce in
|
||||
[#24762](https://github.com/google-gemini/gemini-cli/pull/24762)
|
||||
- fix(cli): respect global environment variable allowlist by @scidomino in
|
||||
[#24767](https://github.com/google-gemini/gemini-cli/pull/24767)
|
||||
- fix(cli): ensure skills list outputs to stdout in non-interactive environments
|
||||
by @spencer426 in
|
||||
[#24566](https://github.com/google-gemini/gemini-cli/pull/24566)
|
||||
- Add an eval for and fix unsafe cloning behavior. by @gundermanc in
|
||||
[#24457](https://github.com/google-gemini/gemini-cli/pull/24457)
|
||||
- fix(policy): allow complete_task in plan mode by @abhipatel12 in
|
||||
[#24771](https://github.com/google-gemini/gemini-cli/pull/24771)
|
||||
- feat(telemetry): add browser agent clearcut metrics by @gsquared94 in
|
||||
[#24688](https://github.com/google-gemini/gemini-cli/pull/24688)
|
||||
- feat(cli): support selective topic expansion and click-to-expand by
|
||||
@Abhijit-2592 in
|
||||
[#24793](https://github.com/google-gemini/gemini-cli/pull/24793)
|
||||
- temporarily disable sandbox integration test on windows by @ehedlund in
|
||||
[#24786](https://github.com/google-gemini/gemini-cli/pull/24786)
|
||||
- Remove flakey test by @scidomino in
|
||||
[#24837](https://github.com/google-gemini/gemini-cli/pull/24837)
|
||||
- Alisa/approve button by @alisa-alisa in
|
||||
[#24645](https://github.com/google-gemini/gemini-cli/pull/24645)
|
||||
- feat(hooks): display hook system messages in UI by @mbleigh in
|
||||
[#24616](https://github.com/google-gemini/gemini-cli/pull/24616)
|
||||
- fix(core): propagate BeforeModel hook model override end-to-end by @krishdef7
|
||||
in [#24784](https://github.com/google-gemini/gemini-cli/pull/24784)
|
||||
- chore: fix formatting for behavioral eval skill reference file by @abhipatel12
|
||||
in [#24846](https://github.com/google-gemini/gemini-cli/pull/24846)
|
||||
- fix: use directory junctions on Windows for skill linking by @enjoykumawat in
|
||||
[#24823](https://github.com/google-gemini/gemini-cli/pull/24823)
|
||||
- fix(cli): prevent multiple banner increments on remount by @sehoon38 in
|
||||
[#24843](https://github.com/google-gemini/gemini-cli/pull/24843)
|
||||
- feat(acp): add /help command by @sripasg in
|
||||
[#24839](https://github.com/google-gemini/gemini-cli/pull/24839)
|
||||
- fix(core): remove tmux alternate buffer warning by @jackwotherspoon in
|
||||
[#24852](https://github.com/google-gemini/gemini-cli/pull/24852)
|
||||
- Improve sandbox error matching and caching by @DavidAPierce in
|
||||
[#24550](https://github.com/google-gemini/gemini-cli/pull/24550)
|
||||
- feat(core): add agent protocol UI types and experimental flag by @mbleigh in
|
||||
[#24275](https://github.com/google-gemini/gemini-cli/pull/24275)
|
||||
- feat(core): use experiment flags for default fetch timeouts by @yunaseoul in
|
||||
[#24261](https://github.com/google-gemini/gemini-cli/pull/24261)
|
||||
- Revert "fix(ui): improve narration suppression and reduce flicker (#2… by
|
||||
@gundermanc in
|
||||
[#24857](https://github.com/google-gemini/gemini-cli/pull/24857)
|
||||
- refactor(cli): remove duplication in interactive shell awaiting input hint by
|
||||
@JayadityaGit in
|
||||
[#24801](https://github.com/google-gemini/gemini-cli/pull/24801)
|
||||
- refactor(core): make LegacyAgentSession dependencies optional by @mbleigh in
|
||||
[#24287](https://github.com/google-gemini/gemini-cli/pull/24287)
|
||||
- Changelog for v0.37.0-preview.2 by @gemini-cli-robot in
|
||||
[#24848](https://github.com/google-gemini/gemini-cli/pull/24848)
|
||||
- fix(cli): always show shell command description or actual command by @jacob314
|
||||
in [#24774](https://github.com/google-gemini/gemini-cli/pull/24774)
|
||||
- Added flag for ept size and increased default size by @devr0306 in
|
||||
[#24859](https://github.com/google-gemini/gemini-cli/pull/24859)
|
||||
- fix(core): dispose Scheduler to prevent McpProgress listener leak by
|
||||
@Anjaligarhwal in
|
||||
[#24870](https://github.com/google-gemini/gemini-cli/pull/24870)
|
||||
- fix(cli): switch default back to terminalBuffer=false and fix regressions
|
||||
introduced for that mode by @jacob314 in
|
||||
[#24873](https://github.com/google-gemini/gemini-cli/pull/24873)
|
||||
- feat(cli): switch to ctrl+g from ctrl-x by @jacob314 in
|
||||
[#24861](https://github.com/google-gemini/gemini-cli/pull/24861)
|
||||
- fix: isolate concurrent browser agent instances by @gsquared94 in
|
||||
[#24794](https://github.com/google-gemini/gemini-cli/pull/24794)
|
||||
- docs: update MCP server OAuth redirect port documentation by @adamfweidman in
|
||||
[#24844](https://github.com/google-gemini/gemini-cli/pull/24844)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.36.0-preview.8...v0.37.0-preview.2
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.37.0-preview.2...v0.38.0-preview.0
|
||||
|
||||
Generated
+17
-45
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"ink": "npm:@jrichman/ink@6.6.7",
|
||||
"ink": "npm:@jrichman/ink@6.6.9",
|
||||
"latest-version": "^9.0.0",
|
||||
"node-fetch-native": "^1.6.7",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
@@ -446,8 +446,7 @@
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
|
||||
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)",
|
||||
"peer": true
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@bundled-es-modules/cookie": {
|
||||
"version": "2.0.1",
|
||||
@@ -1450,7 +1449,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
|
||||
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
@@ -2157,7 +2155,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",
|
||||
@@ -2338,7 +2335,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"
|
||||
}
|
||||
@@ -2388,7 +2384,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
|
||||
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
@@ -2763,7 +2758,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
|
||||
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -2797,7 +2791,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz",
|
||||
"integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/resources": "2.5.0"
|
||||
@@ -2852,7 +2845,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz",
|
||||
"integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/resources": "2.5.0",
|
||||
@@ -4089,7 +4081,6 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4364,7 +4355,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",
|
||||
@@ -5238,7 +5228,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"
|
||||
},
|
||||
@@ -7379,8 +7368,7 @@
|
||||
"version": "0.0.1581282",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
|
||||
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
@@ -7964,7 +7952,6 @@
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8482,7 +8469,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",
|
||||
@@ -9795,7 +9781,6 @@
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
|
||||
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -10070,11 +10055,10 @@
|
||||
},
|
||||
"node_modules/ink": {
|
||||
"name": "@jrichman/ink",
|
||||
"version": "6.6.7",
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.7.tgz",
|
||||
"integrity": "sha512-bDzQLpLzK/dn9Ur/Ku88ZZR9totVcMGrGYAgPHidsAAbe9NKztU1fggj/iu0wRp5g1kBeALb3cfagFGdDxAU1w==",
|
||||
"version": "6.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.9.tgz",
|
||||
"integrity": "sha512-RL9sSiLQZECnjbmBwjIHOp8yVGdWF7C/uifg7ISv/e+F3nLNsfl7FdUFQs8iZARFMJAYxMFpxW6OW+HSt9drwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-escapes": "^7.0.0",
|
||||
"ansi-styles": "^6.2.3",
|
||||
@@ -13848,7 +13832,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13859,7 +13842,6 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -16009,7 +15991,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16232,8 +16213,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -16241,7 +16221,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"
|
||||
@@ -16407,7 +16386,6 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16630,7 +16608,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",
|
||||
@@ -16744,7 +16721,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16757,7 +16733,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",
|
||||
@@ -17405,7 +17380,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"
|
||||
}
|
||||
@@ -17421,7 +17395,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -17536,7 +17510,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
@@ -17558,7 +17532,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^12.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "npm:@jrichman/ink@6.6.7",
|
||||
"ink": "npm:@jrichman/ink@6.6.9",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
@@ -17708,7 +17682,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
@@ -17849,7 +17823,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
|
||||
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
@@ -17953,7 +17926,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -17976,7 +17948,7 @@
|
||||
},
|
||||
"packages/devtools": {
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.16.0"
|
||||
@@ -17991,7 +17963,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -18008,7 +17980,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -18026,7 +17998,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.39.0-nightly.20260408.e77b22e63"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
@@ -71,7 +71,7 @@
|
||||
"pre-commit": "node scripts/pre-commit.js"
|
||||
},
|
||||
"overrides": {
|
||||
"ink": "npm:@jrichman/ink@6.6.7",
|
||||
"ink": "npm:@jrichman/ink@6.6.9",
|
||||
"wrap-ansi": "9.0.2",
|
||||
"cliui": {
|
||||
"wrap-ansi": "7.0.0"
|
||||
@@ -139,7 +139,7 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ink": "npm:@jrichman/ink@6.6.7",
|
||||
"ink": "npm:@jrichman/ink@6.6.9",
|
||||
"latest-version": "^9.0.0",
|
||||
"node-fetch-native": "^1.6.7",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -27,7 +27,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.39.0-nightly.20260408.e77b22e63"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
@@ -49,7 +49,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^12.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "npm:@jrichman/ink@6.6.7",
|
||||
"ink": "npm:@jrichman/ink@6.6.9",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import {
|
||||
addMemory,
|
||||
listInboxSkills,
|
||||
listMemoryFiles,
|
||||
refreshMemory,
|
||||
showMemory,
|
||||
@@ -30,6 +31,7 @@ export class MemoryCommand implements Command {
|
||||
new RefreshMemoryCommand(),
|
||||
new ListMemoryCommand(),
|
||||
new AddMemoryCommand(),
|
||||
new InboxMemoryCommand(),
|
||||
];
|
||||
readonly requiresWorkspace = true;
|
||||
|
||||
@@ -122,3 +124,39 @@ export class AddMemoryCommand implements Command {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class InboxMemoryCommand implements Command {
|
||||
readonly name = 'memory inbox';
|
||||
readonly description =
|
||||
'Lists skills extracted from past sessions that are pending review.';
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
if (!context.agentContext.config.isMemoryManagerEnabled()) {
|
||||
return {
|
||||
name: this.name,
|
||||
data: 'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
|
||||
};
|
||||
}
|
||||
|
||||
const skills = await listInboxSkills(context.agentContext.config);
|
||||
|
||||
if (skills.length === 0) {
|
||||
return { name: this.name, data: 'No extracted skills in inbox.' };
|
||||
}
|
||||
|
||||
const lines = skills.map((s) => {
|
||||
const date = s.extractedAt
|
||||
? ` (extracted: ${new Date(s.extractedAt).toLocaleDateString()})`
|
||||
: '';
|
||||
return `- **${s.name}**: ${s.description}${date}`;
|
||||
});
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
data: `Skill inbox (${skills.length}):\n${lines.join('\n')}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,4 +457,78 @@ describe('memoryCommand', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/memory inbox', () => {
|
||||
let inboxCommand: SlashCommand;
|
||||
|
||||
beforeEach(() => {
|
||||
inboxCommand = memoryCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'inbox',
|
||||
)!;
|
||||
expect(inboxCommand).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return custom_dialog when config is available and flag is enabled', () => {
|
||||
if (!inboxCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const mockConfig = {
|
||||
reloadSkills: vi.fn(),
|
||||
isMemoryManagerEnabled: vi.fn().mockReturnValue(true),
|
||||
};
|
||||
const context = createMockCommandContext({
|
||||
services: {
|
||||
agentContext: { config: mockConfig },
|
||||
},
|
||||
ui: {
|
||||
removeComponent: vi.fn(),
|
||||
reloadCommands: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
const result = inboxCommand.action(context, '');
|
||||
|
||||
expect(result).toHaveProperty('type', 'custom_dialog');
|
||||
expect(result).toHaveProperty('component');
|
||||
});
|
||||
|
||||
it('should return info message when memory manager is disabled', () => {
|
||||
if (!inboxCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const mockConfig = {
|
||||
isMemoryManagerEnabled: vi.fn().mockReturnValue(false),
|
||||
};
|
||||
const context = createMockCommandContext({
|
||||
services: {
|
||||
agentContext: { config: mockConfig },
|
||||
},
|
||||
});
|
||||
|
||||
const result = inboxCommand.action(context, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when config is not loaded', () => {
|
||||
if (!inboxCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const context = createMockCommandContext({
|
||||
services: {
|
||||
agentContext: null,
|
||||
},
|
||||
});
|
||||
|
||||
const result = inboxCommand.action(context, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Config not loaded.',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
addMemory,
|
||||
listMemoryFiles,
|
||||
@@ -13,9 +14,11 @@ import {
|
||||
import { MessageType } from '../types.js';
|
||||
import {
|
||||
CommandKind,
|
||||
type OpenCustomDialogActionReturn,
|
||||
type SlashCommand,
|
||||
type SlashCommandActionReturn,
|
||||
} from './types.js';
|
||||
import { SkillInboxDialog } from '../components/SkillInboxDialog.js';
|
||||
|
||||
export const memoryCommand: SlashCommand = {
|
||||
name: 'memory',
|
||||
@@ -124,5 +127,45 @@ export const memoryCommand: SlashCommand = {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'inbox',
|
||||
description:
|
||||
'Review skills extracted from past sessions and move them to global or project skills',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (
|
||||
context,
|
||||
): OpenCustomDialogActionReturn | SlashCommandActionReturn | void => {
|
||||
const config = context.services.agentContext?.config;
|
||||
if (!config) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Config not loaded.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.isMemoryManagerEnabled()) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'custom_dialog',
|
||||
component: React.createElement(SkillInboxDialog, {
|
||||
config,
|
||||
onClose: () => context.ui.removeComponent(),
|
||||
onReloadSkills: async () => {
|
||||
await config.reloadSkills();
|
||||
context.ui.reloadCommands();
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -2318,6 +2318,11 @@ describe('InputPrompt', () => {
|
||||
text: '',
|
||||
visualCursor: [0, 0],
|
||||
},
|
||||
{
|
||||
name: 'on a full-width line with no whitespace',
|
||||
text: 'x'.repeat(94),
|
||||
visualCursor: [0, 94],
|
||||
},
|
||||
{
|
||||
name: 'on a space between words',
|
||||
text: 'hello world',
|
||||
|
||||
@@ -1849,7 +1849,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
? `line-${item.absoluteVisualIdx}`
|
||||
: `ghost-${item.index}`
|
||||
}
|
||||
width={inputWidth}
|
||||
width={inputWidth + 1 /* space for scrollbar */}
|
||||
backgroundColor={listBackgroundColor}
|
||||
containerHeight={Math.min(
|
||||
buffer.viewportHeight,
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { act } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Config, InboxSkill } from '@google/gemini-cli-core';
|
||||
import {
|
||||
dismissInboxSkill,
|
||||
listInboxSkills,
|
||||
moveInboxSkill,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { SkillInboxDialog } from './SkillInboxDialog.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const original =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
|
||||
return {
|
||||
...original,
|
||||
dismissInboxSkill: vi.fn(),
|
||||
listInboxSkills: vi.fn(),
|
||||
moveInboxSkill: vi.fn(),
|
||||
getErrorMessage: vi.fn((error: unknown) =>
|
||||
error instanceof Error ? error.message : String(error),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const mockListInboxSkills = vi.mocked(listInboxSkills);
|
||||
const mockMoveInboxSkill = vi.mocked(moveInboxSkill);
|
||||
const mockDismissInboxSkill = vi.mocked(dismissInboxSkill);
|
||||
|
||||
const inboxSkill: InboxSkill = {
|
||||
dirName: 'inbox-skill',
|
||||
name: 'Inbox Skill',
|
||||
description: 'A test skill',
|
||||
extractedAt: '2025-01-15T10:00:00Z',
|
||||
};
|
||||
|
||||
describe('SkillInboxDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockListInboxSkills.mockResolvedValue([inboxSkill]);
|
||||
mockMoveInboxSkill.mockResolvedValue({
|
||||
success: true,
|
||||
message: 'Moved "inbox-skill" to ~/.gemini/skills.',
|
||||
});
|
||||
mockDismissInboxSkill.mockResolvedValue({
|
||||
success: true,
|
||||
message: 'Dismissed "inbox-skill" from inbox.',
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the project destination when the workspace is untrusted', async () => {
|
||||
const config = {
|
||||
isTrustedFolder: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Config;
|
||||
const onReloadSkills = vi.fn().mockResolvedValue(undefined);
|
||||
const { lastFrame, stdin, unmount, waitUntilReady } = await act(async () =>
|
||||
renderWithProviders(
|
||||
<SkillInboxDialog
|
||||
config={config}
|
||||
onClose={vi.fn()}
|
||||
onReloadSkills={onReloadSkills}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Inbox Skill');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = lastFrame();
|
||||
expect(frame).toContain('Project');
|
||||
expect(frame).toContain('unavailable until this workspace is trusted');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x1b[B');
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDismissInboxSkill).toHaveBeenCalledWith(config, 'inbox-skill');
|
||||
});
|
||||
expect(mockMoveInboxSkill).not.toHaveBeenCalled();
|
||||
expect(onReloadSkills).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows inline feedback when moving a skill throws', async () => {
|
||||
mockMoveInboxSkill.mockRejectedValue(new Error('permission denied'));
|
||||
|
||||
const config = {
|
||||
isTrustedFolder: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
const { lastFrame, stdin, unmount, waitUntilReady } = await act(async () =>
|
||||
renderWithProviders(
|
||||
<SkillInboxDialog
|
||||
config={config}
|
||||
onClose={vi.fn()}
|
||||
onReloadSkills={vi.fn().mockResolvedValue(undefined)}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Inbox Skill');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = lastFrame();
|
||||
expect(frame).toContain('Move "Inbox Skill"');
|
||||
expect(frame).toContain('Failed to install skill: permission denied');
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows inline feedback when reloading skills fails after a move', async () => {
|
||||
const config = {
|
||||
isTrustedFolder: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config;
|
||||
const onReloadSkills = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('reload hook failed'));
|
||||
const { lastFrame, stdin, unmount, waitUntilReady } = await act(async () =>
|
||||
renderWithProviders(
|
||||
<SkillInboxDialog
|
||||
config={config}
|
||||
onClose={vi.fn()}
|
||||
onReloadSkills={onReloadSkills}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Inbox Skill');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain(
|
||||
'Moved "inbox-skill" to ~/.gemini/skills. Failed to reload skills: reload hook failed',
|
||||
);
|
||||
});
|
||||
expect(onReloadSkills).toHaveBeenCalledTimes(1);
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { Command } from '../key/keyMatchers.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
import { BaseSelectionList } from './shared/BaseSelectionList.js';
|
||||
import type { SelectionListItem } from '../hooks/useSelectionList.js';
|
||||
import { DialogFooter } from './shared/DialogFooter.js';
|
||||
import {
|
||||
type Config,
|
||||
type InboxSkill,
|
||||
type InboxSkillDestination,
|
||||
getErrorMessage,
|
||||
listInboxSkills,
|
||||
moveInboxSkill,
|
||||
dismissInboxSkill,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
type Phase = 'list' | 'action';
|
||||
|
||||
interface DestinationChoice {
|
||||
destination: InboxSkillDestination | 'dismiss';
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const DESTINATION_CHOICES: DestinationChoice[] = [
|
||||
{
|
||||
destination: 'global',
|
||||
label: 'Global',
|
||||
description: '~/.gemini/skills — available in all projects',
|
||||
},
|
||||
{
|
||||
destination: 'project',
|
||||
label: 'Project',
|
||||
description: '.gemini/skills — available in this workspace',
|
||||
},
|
||||
{
|
||||
destination: 'dismiss',
|
||||
label: 'Dismiss',
|
||||
description: 'Delete from inbox',
|
||||
},
|
||||
];
|
||||
|
||||
function formatDate(isoString: string): string {
|
||||
try {
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
} catch {
|
||||
return isoString;
|
||||
}
|
||||
}
|
||||
|
||||
interface SkillInboxDialogProps {
|
||||
config: Config;
|
||||
onClose: () => void;
|
||||
onReloadSkills: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const SkillInboxDialog: React.FC<SkillInboxDialogProps> = ({
|
||||
config,
|
||||
onClose,
|
||||
onReloadSkills,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const isTrustedFolder = config.isTrustedFolder();
|
||||
const [phase, setPhase] = useState<Phase>('list');
|
||||
const [skills, setSkills] = useState<InboxSkill[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedSkill, setSelectedSkill] = useState<InboxSkill | null>(null);
|
||||
const [feedback, setFeedback] = useState<{
|
||||
text: string;
|
||||
isError: boolean;
|
||||
} | null>(null);
|
||||
|
||||
// Load inbox skills on mount
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await listInboxSkills(config);
|
||||
if (!cancelled) {
|
||||
setSkills(result);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setSkills([]);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [config]);
|
||||
|
||||
const skillItems: Array<SelectionListItem<InboxSkill>> = useMemo(
|
||||
() =>
|
||||
skills.map((skill) => ({
|
||||
key: skill.dirName,
|
||||
value: skill,
|
||||
})),
|
||||
[skills],
|
||||
);
|
||||
|
||||
const destinationItems: Array<SelectionListItem<DestinationChoice>> = useMemo(
|
||||
() =>
|
||||
DESTINATION_CHOICES.map((choice) => {
|
||||
if (choice.destination === 'project' && !isTrustedFolder) {
|
||||
return {
|
||||
key: choice.destination,
|
||||
value: {
|
||||
...choice,
|
||||
description:
|
||||
'.gemini/skills — unavailable until this workspace is trusted',
|
||||
},
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
key: choice.destination,
|
||||
value: choice,
|
||||
};
|
||||
}),
|
||||
[isTrustedFolder],
|
||||
);
|
||||
|
||||
const handleSelectSkill = useCallback((skill: InboxSkill) => {
|
||||
setSelectedSkill(skill);
|
||||
setFeedback(null);
|
||||
setPhase('action');
|
||||
}, []);
|
||||
|
||||
const handleSelectDestination = useCallback(
|
||||
(choice: DestinationChoice) => {
|
||||
if (!selectedSkill) return;
|
||||
|
||||
if (choice.destination === 'project' && !config.isTrustedFolder()) {
|
||||
setFeedback({
|
||||
text: 'Project skills are unavailable until this workspace is trusted.',
|
||||
isError: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setFeedback(null);
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
let result: { success: boolean; message: string };
|
||||
if (choice.destination === 'dismiss') {
|
||||
result = await dismissInboxSkill(config, selectedSkill.dirName);
|
||||
} else {
|
||||
result = await moveInboxSkill(
|
||||
config,
|
||||
selectedSkill.dirName,
|
||||
choice.destination,
|
||||
);
|
||||
}
|
||||
|
||||
setFeedback({ text: result.message, isError: !result.success });
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the skill from the local list.
|
||||
setSkills((prev) =>
|
||||
prev.filter((skill) => skill.dirName !== selectedSkill.dirName),
|
||||
);
|
||||
setSelectedSkill(null);
|
||||
setPhase('list');
|
||||
|
||||
if (choice.destination === 'dismiss') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await onReloadSkills();
|
||||
} catch (error) {
|
||||
setFeedback({
|
||||
text: `${result.message} Failed to reload skills: ${getErrorMessage(error)}`,
|
||||
isError: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const operation =
|
||||
choice.destination === 'dismiss'
|
||||
? 'dismiss skill'
|
||||
: 'install skill';
|
||||
setFeedback({
|
||||
text: `Failed to ${operation}: ${getErrorMessage(error)}`,
|
||||
isError: true,
|
||||
});
|
||||
}
|
||||
})();
|
||||
},
|
||||
[config, selectedSkill, onReloadSkills],
|
||||
);
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (keyMatchers[Command.ESCAPE](key)) {
|
||||
if (phase === 'action') {
|
||||
setPhase('list');
|
||||
setSelectedSkill(null);
|
||||
setFeedback(null);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ isActive: true, priority: true },
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
paddingX={2}
|
||||
paddingY={1}
|
||||
>
|
||||
<Text>Loading inbox…</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (skills.length === 0 && !feedback) {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
paddingX={2}
|
||||
paddingY={1}
|
||||
>
|
||||
<Text bold>Skill Inbox</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
No extracted skills in inbox.
|
||||
</Text>
|
||||
</Box>
|
||||
<DialogFooter primaryAction="Esc to close" cancelAction="" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
paddingX={2}
|
||||
paddingY={1}
|
||||
width="100%"
|
||||
>
|
||||
{phase === 'list' ? (
|
||||
<>
|
||||
<Text bold>
|
||||
Skill Inbox ({skills.length} skill{skills.length !== 1 ? 's' : ''})
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
Skills extracted from past sessions. Select one to move or dismiss.
|
||||
</Text>
|
||||
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<BaseSelectionList<InboxSkill>
|
||||
items={skillItems}
|
||||
onSelect={handleSelectSkill}
|
||||
isFocused={true}
|
||||
showNumbers={true}
|
||||
showScrollArrows={true}
|
||||
maxItemsToShow={8}
|
||||
renderItem={(item, { titleColor }) => (
|
||||
<Box flexDirection="column" minHeight={2}>
|
||||
<Text color={titleColor} bold>
|
||||
{item.value.name}
|
||||
</Text>
|
||||
<Box flexDirection="row">
|
||||
<Text color={theme.text.secondary} wrap="wrap">
|
||||
{item.value.description}
|
||||
</Text>
|
||||
{item.value.extractedAt && (
|
||||
<Text color={theme.text.secondary}>
|
||||
{' · '}
|
||||
{formatDate(item.value.extractedAt)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{feedback && (
|
||||
<Box marginTop={1}>
|
||||
<Text
|
||||
color={
|
||||
feedback.isError ? theme.status.error : theme.status.success
|
||||
}
|
||||
>
|
||||
{feedback.isError ? '✗ ' : '✓ '}
|
||||
{feedback.text}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<DialogFooter
|
||||
primaryAction="Enter to select"
|
||||
cancelAction="Esc to close"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text bold>Move "{selectedSkill?.name}"</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
Choose where to install this skill.
|
||||
</Text>
|
||||
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<BaseSelectionList<DestinationChoice>
|
||||
items={destinationItems}
|
||||
onSelect={handleSelectDestination}
|
||||
isFocused={true}
|
||||
showNumbers={true}
|
||||
renderItem={(item, { titleColor }) => (
|
||||
<Box flexDirection="column" minHeight={2}>
|
||||
<Text color={titleColor} bold>
|
||||
{item.value.label}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
{item.value.description}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{feedback && (
|
||||
<Box marginTop={1}>
|
||||
<Text
|
||||
color={
|
||||
feedback.isError ? theme.status.error : theme.status.success
|
||||
}
|
||||
>
|
||||
{feedback.isError ? '✗ ' : '✓ '}
|
||||
{feedback.text}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<DialogFooter
|
||||
primaryAction="Enter to confirm"
|
||||
cancelAction="Esc to go back"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<text x="36" y="19" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</text>
|
||||
<rect x="882" y="17" width="9" height="17" fill="#ffffff" />
|
||||
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -72,6 +72,12 @@ exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios >
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'on a full-width line with no whitespa…' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'on a highlighted token' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > run @path/to/file │
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"description": "Gemini CLI Core",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import {
|
||||
addMemory,
|
||||
dismissInboxSkill,
|
||||
listInboxSkills,
|
||||
listMemoryFiles,
|
||||
moveInboxSkill,
|
||||
refreshMemory,
|
||||
showMemory,
|
||||
} from './memory.js';
|
||||
@@ -18,6 +25,12 @@ vi.mock('../utils/memoryDiscovery.js', () => ({
|
||||
refreshServerHierarchicalMemory: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../config/storage.js', () => ({
|
||||
Storage: {
|
||||
getUserSkillsDir: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockRefresh = vi.mocked(memoryDiscovery.refreshServerHierarchicalMemory);
|
||||
|
||||
describe('memory commands', () => {
|
||||
@@ -202,4 +215,317 @@ describe('memory commands', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('listInboxSkills', () => {
|
||||
let tmpDir: string;
|
||||
let skillsDir: string;
|
||||
let memoryTempDir: string;
|
||||
let inboxConfig: Config;
|
||||
|
||||
async function writeSkillMd(
|
||||
dirName: string,
|
||||
name: string,
|
||||
description: string,
|
||||
): Promise<void> {
|
||||
const dir = path.join(skillsDir, dirName);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(dir, 'SKILL.md'),
|
||||
`---\nname: ${name}\ndescription: ${description}\n---\nBody content here\n`,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'inbox-test-'));
|
||||
skillsDir = path.join(tmpDir, 'skills-memory');
|
||||
memoryTempDir = path.join(tmpDir, 'memory-temp');
|
||||
await fs.mkdir(skillsDir, { recursive: true });
|
||||
await fs.mkdir(memoryTempDir, { recursive: true });
|
||||
|
||||
inboxConfig = {
|
||||
storage: {
|
||||
getProjectSkillsMemoryDir: () => skillsDir,
|
||||
getProjectMemoryTempDir: () => memoryTempDir,
|
||||
getProjectSkillsDir: () => path.join(tmpDir, 'project-skills'),
|
||||
},
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should return inbox skills with name, description, and extractedAt', async () => {
|
||||
await writeSkillMd('my-skill', 'my-skill', 'A test skill');
|
||||
await writeSkillMd('other-skill', 'other-skill', 'Another skill');
|
||||
|
||||
const stateContent = JSON.stringify({
|
||||
runs: [
|
||||
{
|
||||
runAt: '2025-01-15T10:00:00Z',
|
||||
sessionIds: ['sess-1'],
|
||||
skillsCreated: ['my-skill'],
|
||||
},
|
||||
{
|
||||
runAt: '2025-01-16T12:00:00Z',
|
||||
sessionIds: ['sess-2'],
|
||||
skillsCreated: ['other-skill'],
|
||||
},
|
||||
],
|
||||
});
|
||||
await fs.writeFile(
|
||||
path.join(memoryTempDir, '.extraction-state.json'),
|
||||
stateContent,
|
||||
);
|
||||
|
||||
const skills = await listInboxSkills(inboxConfig);
|
||||
|
||||
expect(skills).toHaveLength(2);
|
||||
const mySkill = skills.find((s) => s.dirName === 'my-skill');
|
||||
expect(mySkill).toBeDefined();
|
||||
expect(mySkill!.name).toBe('my-skill');
|
||||
expect(mySkill!.description).toBe('A test skill');
|
||||
expect(mySkill!.extractedAt).toBe('2025-01-15T10:00:00Z');
|
||||
|
||||
const otherSkill = skills.find((s) => s.dirName === 'other-skill');
|
||||
expect(otherSkill).toBeDefined();
|
||||
expect(otherSkill!.name).toBe('other-skill');
|
||||
expect(otherSkill!.description).toBe('Another skill');
|
||||
expect(otherSkill!.extractedAt).toBe('2025-01-16T12:00:00Z');
|
||||
});
|
||||
|
||||
it('should return an empty array when the inbox is empty', async () => {
|
||||
const skills = await listInboxSkills(inboxConfig);
|
||||
expect(skills).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array when the inbox directory does not exist', async () => {
|
||||
const missingConfig = {
|
||||
storage: {
|
||||
getProjectSkillsMemoryDir: () => path.join(tmpDir, 'nonexistent-dir'),
|
||||
getProjectMemoryTempDir: () => memoryTempDir,
|
||||
},
|
||||
} as unknown as Config;
|
||||
|
||||
const skills = await listInboxSkills(missingConfig);
|
||||
expect(skills).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveInboxSkill', () => {
|
||||
let tmpDir: string;
|
||||
let skillsDir: string;
|
||||
let globalSkillsDir: string;
|
||||
let projectSkillsDir: string;
|
||||
let moveConfig: Config;
|
||||
|
||||
async function writeSkillMd(
|
||||
dirName: string,
|
||||
name: string,
|
||||
description: string,
|
||||
): Promise<void> {
|
||||
const dir = path.join(skillsDir, dirName);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(dir, 'SKILL.md'),
|
||||
`---\nname: ${name}\ndescription: ${description}\n---\nBody content here\n`,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'move-test-'));
|
||||
skillsDir = path.join(tmpDir, 'skills-memory');
|
||||
globalSkillsDir = path.join(tmpDir, 'global-skills');
|
||||
projectSkillsDir = path.join(tmpDir, 'project-skills');
|
||||
await fs.mkdir(skillsDir, { recursive: true });
|
||||
|
||||
moveConfig = {
|
||||
storage: {
|
||||
getProjectSkillsMemoryDir: () => skillsDir,
|
||||
getProjectSkillsDir: () => projectSkillsDir,
|
||||
},
|
||||
} as unknown as Config;
|
||||
|
||||
vi.mocked(Storage.getUserSkillsDir).mockReturnValue(globalSkillsDir);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should move a skill to global skills directory', async () => {
|
||||
await writeSkillMd('my-skill', 'my-skill', 'A test skill');
|
||||
|
||||
const result = await moveInboxSkill(moveConfig, 'my-skill', 'global');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Moved "my-skill" to ~/.gemini/skills.');
|
||||
|
||||
// Verify the skill was copied to global
|
||||
const targetSkill = await fs.readFile(
|
||||
path.join(globalSkillsDir, 'my-skill', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(targetSkill).toContain('name: my-skill');
|
||||
|
||||
// Verify the skill was removed from inbox
|
||||
await expect(
|
||||
fs.access(path.join(skillsDir, 'my-skill')),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should move a skill to project skills directory', async () => {
|
||||
await writeSkillMd('my-skill', 'my-skill', 'A test skill');
|
||||
|
||||
const result = await moveInboxSkill(moveConfig, 'my-skill', 'project');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Moved "my-skill" to .gemini/skills.');
|
||||
|
||||
// Verify the skill was copied to project
|
||||
const targetSkill = await fs.readFile(
|
||||
path.join(projectSkillsDir, 'my-skill', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(targetSkill).toContain('name: my-skill');
|
||||
|
||||
// Verify the skill was removed from inbox
|
||||
await expect(
|
||||
fs.access(path.join(skillsDir, 'my-skill')),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should return an error when the source skill does not exist', async () => {
|
||||
const result = await moveInboxSkill(moveConfig, 'nonexistent', 'global');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Skill "nonexistent" not found in inbox.');
|
||||
});
|
||||
|
||||
it('should reject invalid skill directory names', async () => {
|
||||
const result = await moveInboxSkill(moveConfig, '../escape', 'global');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Invalid skill name.');
|
||||
});
|
||||
|
||||
it('should return an error when the target already exists', async () => {
|
||||
await writeSkillMd('my-skill', 'my-skill', 'A test skill');
|
||||
|
||||
// Pre-create the target
|
||||
const targetDir = path.join(globalSkillsDir, 'my-skill');
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
await fs.writeFile(path.join(targetDir, 'SKILL.md'), 'existing content');
|
||||
|
||||
const result = await moveInboxSkill(moveConfig, 'my-skill', 'global');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(
|
||||
'A skill named "my-skill" already exists in global skills.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect conflicts based on the normalized skill name', async () => {
|
||||
await writeSkillMd(
|
||||
'inbox-skill',
|
||||
'gke:prs-troubleshooter',
|
||||
'A test skill',
|
||||
);
|
||||
await fs.mkdir(
|
||||
path.join(globalSkillsDir, 'existing-gke-prs-troubleshooter'),
|
||||
{ recursive: true },
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(
|
||||
globalSkillsDir,
|
||||
'existing-gke-prs-troubleshooter',
|
||||
'SKILL.md',
|
||||
),
|
||||
[
|
||||
'---',
|
||||
'name: gke-prs-troubleshooter',
|
||||
'description: Existing skill',
|
||||
'---',
|
||||
'Existing body content',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const result = await moveInboxSkill(moveConfig, 'inbox-skill', 'global');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(
|
||||
'A skill named "gke-prs-troubleshooter" already exists in global skills.',
|
||||
);
|
||||
await expect(
|
||||
fs.access(path.join(skillsDir, 'inbox-skill', 'SKILL.md')),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(
|
||||
fs.access(path.join(globalSkillsDir, 'inbox-skill')),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismissInboxSkill', () => {
|
||||
let tmpDir: string;
|
||||
let skillsDir: string;
|
||||
let dismissConfig: Config;
|
||||
|
||||
async function writeSkillMd(
|
||||
dirName: string,
|
||||
name: string,
|
||||
description: string,
|
||||
): Promise<void> {
|
||||
const dir = path.join(skillsDir, dirName);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(dir, 'SKILL.md'),
|
||||
`---\nname: ${name}\ndescription: ${description}\n---\nBody content here\n`,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dismiss-test-'));
|
||||
skillsDir = path.join(tmpDir, 'skills-memory');
|
||||
await fs.mkdir(skillsDir, { recursive: true });
|
||||
|
||||
dismissConfig = {
|
||||
storage: {
|
||||
getProjectSkillsMemoryDir: () => skillsDir,
|
||||
},
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should remove a skill from the inbox', async () => {
|
||||
await writeSkillMd('my-skill', 'my-skill', 'A test skill');
|
||||
|
||||
const result = await dismissInboxSkill(dismissConfig, 'my-skill');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Dismissed "my-skill" from inbox.');
|
||||
|
||||
// Verify the skill directory was removed
|
||||
await expect(
|
||||
fs.access(path.join(skillsDir, 'my-skill')),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should return an error when the skill does not exist', async () => {
|
||||
const result = await dismissInboxSkill(dismissConfig, 'nonexistent');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Skill "nonexistent" not found in inbox.');
|
||||
});
|
||||
|
||||
it('should reject invalid skill directory names', async () => {
|
||||
const result = await dismissInboxSkill(dismissConfig, 'nested\\skill');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Invalid skill name.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,13 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import { flattenMemory } from '../config/memory.js';
|
||||
import { loadSkillFromFile, loadSkillsFromDir } from '../skills/skillLoader.js';
|
||||
import { readExtractionState } from '../services/memoryService.js';
|
||||
import { refreshServerHierarchicalMemory } from '../utils/memoryDiscovery.js';
|
||||
import type { MessageActionReturn, ToolActionReturn } from './types.js';
|
||||
|
||||
@@ -95,3 +100,186 @@ export function listMemoryFiles(config: Config): MessageActionReturn {
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a skill found in the extraction inbox.
|
||||
*/
|
||||
export interface InboxSkill {
|
||||
/** Directory name in the inbox. */
|
||||
dirName: string;
|
||||
/** Skill name from SKILL.md frontmatter. */
|
||||
name: string;
|
||||
/** Skill description from SKILL.md frontmatter. */
|
||||
description: string;
|
||||
/** When the skill was extracted (ISO string), if known. */
|
||||
extractedAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the skill extraction inbox and returns structured data
|
||||
* for each extracted skill.
|
||||
*/
|
||||
export async function listInboxSkills(config: Config): Promise<InboxSkill[]> {
|
||||
const skillsDir = config.storage.getProjectSkillsMemoryDir();
|
||||
|
||||
let entries: Array<import('node:fs').Dirent>;
|
||||
try {
|
||||
entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dirs = entries.filter((e) => e.isDirectory());
|
||||
if (dirs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Load extraction state to get dates
|
||||
const memoryDir = config.storage.getProjectMemoryTempDir();
|
||||
const statePath = path.join(memoryDir, '.extraction-state.json');
|
||||
const state = await readExtractionState(statePath);
|
||||
|
||||
// Build a map: skillDirName → extractedAt
|
||||
const skillDateMap = new Map<string, string>();
|
||||
for (const run of state.runs) {
|
||||
for (const skillName of run.skillsCreated) {
|
||||
skillDateMap.set(skillName, run.runAt);
|
||||
}
|
||||
}
|
||||
|
||||
const skills: InboxSkill[] = [];
|
||||
for (const dir of dirs) {
|
||||
const skillPath = path.join(skillsDir, dir.name, 'SKILL.md');
|
||||
const skillDef = await loadSkillFromFile(skillPath);
|
||||
if (!skillDef) continue;
|
||||
|
||||
skills.push({
|
||||
dirName: dir.name,
|
||||
name: skillDef.name,
|
||||
description: skillDef.description,
|
||||
extractedAt: skillDateMap.get(dir.name),
|
||||
});
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
export type InboxSkillDestination = 'global' | 'project';
|
||||
|
||||
function isValidInboxSkillDirName(dirName: string): boolean {
|
||||
return (
|
||||
dirName.length > 0 &&
|
||||
dirName !== '.' &&
|
||||
dirName !== '..' &&
|
||||
!dirName.includes('/') &&
|
||||
!dirName.includes('\\')
|
||||
);
|
||||
}
|
||||
|
||||
async function getSkillNameForConflictCheck(
|
||||
skillDir: string,
|
||||
fallbackName: string,
|
||||
): Promise<string> {
|
||||
const skill = await loadSkillFromFile(path.join(skillDir, 'SKILL.md'));
|
||||
return skill?.name ?? fallbackName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies an inbox skill to the target skills directory.
|
||||
*/
|
||||
export async function moveInboxSkill(
|
||||
config: Config,
|
||||
dirName: string,
|
||||
destination: InboxSkillDestination,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
if (!isValidInboxSkillDirName(dirName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Invalid skill name.',
|
||||
};
|
||||
}
|
||||
|
||||
const skillsDir = config.storage.getProjectSkillsMemoryDir();
|
||||
const sourcePath = path.join(skillsDir, dirName);
|
||||
|
||||
try {
|
||||
await fs.access(sourcePath);
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
message: `Skill "${dirName}" not found in inbox.`,
|
||||
};
|
||||
}
|
||||
|
||||
const targetBase =
|
||||
destination === 'global'
|
||||
? Storage.getUserSkillsDir()
|
||||
: config.storage.getProjectSkillsDir();
|
||||
const targetPath = path.join(targetBase, dirName);
|
||||
const skillName = await getSkillNameForConflictCheck(sourcePath, dirName);
|
||||
|
||||
try {
|
||||
await fs.access(targetPath);
|
||||
return {
|
||||
success: false,
|
||||
message: `A skill named "${skillName}" already exists in ${destination} skills.`,
|
||||
};
|
||||
} catch {
|
||||
// Target doesn't exist — good
|
||||
}
|
||||
|
||||
const existingTargetSkills = await loadSkillsFromDir(targetBase);
|
||||
if (existingTargetSkills.some((skill) => skill.name === skillName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `A skill named "${skillName}" already exists in ${destination} skills.`,
|
||||
};
|
||||
}
|
||||
|
||||
await fs.mkdir(targetBase, { recursive: true });
|
||||
await fs.cp(sourcePath, targetPath, { recursive: true });
|
||||
|
||||
// Remove from inbox after successful copy
|
||||
await fs.rm(sourcePath, { recursive: true, force: true });
|
||||
|
||||
const label =
|
||||
destination === 'global' ? '~/.gemini/skills' : '.gemini/skills';
|
||||
return {
|
||||
success: true,
|
||||
message: `Moved "${dirName}" to ${label}.`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a skill from the extraction inbox.
|
||||
*/
|
||||
export async function dismissInboxSkill(
|
||||
config: Config,
|
||||
dirName: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
if (!isValidInboxSkillDirName(dirName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Invalid skill name.',
|
||||
};
|
||||
}
|
||||
|
||||
const skillsDir = config.storage.getProjectSkillsMemoryDir();
|
||||
const sourcePath = path.join(skillsDir, dirName);
|
||||
|
||||
try {
|
||||
await fs.access(sourcePath);
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
message: `Skill "${dirName}" not found in inbox.`,
|
||||
};
|
||||
}
|
||||
|
||||
await fs.rm(sourcePath, { recursive: true, force: true });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Dismissed "${dirName}" from inbox.`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type ConversationRecord,
|
||||
} from './chatRecordingService.js';
|
||||
import type { ExtractionState, ExtractionRun } from './memoryService.js';
|
||||
import { coreEvents } from '../utils/events.js';
|
||||
|
||||
// Mock external modules used by startMemoryService
|
||||
vi.mock('../agents/local-executor.js', () => ({
|
||||
@@ -29,6 +30,7 @@ vi.mock('../agents/skill-extraction-agent.js', () => ({
|
||||
promptConfig: { systemPrompt: 'test' },
|
||||
tools: [],
|
||||
outputSchema: {},
|
||||
modelConfig: { model: 'test-model' },
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -51,6 +53,33 @@ vi.mock('../resources/resource-registry.js', () => ({
|
||||
ResourceRegistry: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../policy/policy-engine.js', () => ({
|
||||
PolicyEngine: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../policy/types.js', () => ({
|
||||
PolicyDecision: { ALLOW: 'ALLOW' },
|
||||
}));
|
||||
|
||||
vi.mock('../confirmation-bus/message-bus.js', () => ({
|
||||
MessageBus: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../agents/registry.js', () => ({
|
||||
getModelConfigAlias: vi.fn().mockReturnValue('skill-extraction-config'),
|
||||
}));
|
||||
|
||||
vi.mock('../config/storage.js', () => ({
|
||||
Storage: {
|
||||
getUserSkillsDir: vi.fn().mockReturnValue('/tmp/fake-user-skills'),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../skills/skillLoader.js', () => ({
|
||||
FRONTMATTER_REGEX: /^---\n([\s\S]*?)\n---/,
|
||||
parseFrontmatter: vi.fn().mockReturnValue(null),
|
||||
}));
|
||||
|
||||
vi.mock('../utils/debugLogger.js', () => ({
|
||||
debugLogger: {
|
||||
debug: vi.fn(),
|
||||
@@ -59,6 +88,12 @@ vi.mock('../utils/debugLogger.js', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../utils/events.js', () => ({
|
||||
coreEvents: {
|
||||
emitFeedback: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper to create a minimal ConversationRecord
|
||||
function createConversation(
|
||||
overrides: Partial<ConversationRecord> & { messageCount?: number } = {},
|
||||
@@ -427,6 +462,77 @@ describe('memoryService', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('emits feedback when new skills are created during extraction', async () => {
|
||||
const { startMemoryService } = await import('./memoryService.js');
|
||||
const { LocalAgentExecutor } = await import(
|
||||
'../agents/local-executor.js'
|
||||
);
|
||||
|
||||
// Reset mocks that may carry state from prior tests
|
||||
vi.mocked(coreEvents.emitFeedback).mockClear();
|
||||
vi.mocked(LocalAgentExecutor.create).mockReset();
|
||||
|
||||
const memoryDir = path.join(tmpDir, 'memory4');
|
||||
const skillsDir = path.join(tmpDir, 'skills4');
|
||||
const projectTempDir = path.join(tmpDir, 'temp4');
|
||||
const chatsDir = path.join(projectTempDir, 'chats');
|
||||
await fs.mkdir(memoryDir, { recursive: true });
|
||||
await fs.mkdir(skillsDir, { recursive: true });
|
||||
await fs.mkdir(chatsDir, { recursive: true });
|
||||
|
||||
// Write a valid session with enough messages to pass the filter
|
||||
const conversation = createConversation({
|
||||
sessionId: 'skill-session',
|
||||
messageCount: 20,
|
||||
});
|
||||
await fs.writeFile(
|
||||
path.join(chatsDir, 'session-2025-01-01T00-00-skill001.json'),
|
||||
JSON.stringify(conversation),
|
||||
);
|
||||
|
||||
// Override LocalAgentExecutor.create to return an executor whose run
|
||||
// creates a new skill directory with a SKILL.md in the skillsDir
|
||||
vi.mocked(LocalAgentExecutor.create).mockResolvedValueOnce({
|
||||
run: vi.fn().mockImplementation(async () => {
|
||||
const newSkillDir = path.join(skillsDir, 'my-new-skill');
|
||||
await fs.mkdir(newSkillDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(newSkillDir, 'SKILL.md'),
|
||||
'# My New Skill',
|
||||
);
|
||||
return undefined;
|
||||
}),
|
||||
} as never);
|
||||
|
||||
const mockConfig = {
|
||||
storage: {
|
||||
getProjectMemoryDir: vi.fn().mockReturnValue(memoryDir),
|
||||
getProjectMemoryTempDir: vi.fn().mockReturnValue(memoryDir),
|
||||
getProjectSkillsMemoryDir: vi.fn().mockReturnValue(skillsDir),
|
||||
getProjectTempDir: vi.fn().mockReturnValue(projectTempDir),
|
||||
},
|
||||
getToolRegistry: vi.fn(),
|
||||
getMessageBus: vi.fn(),
|
||||
getGeminiClient: vi.fn(),
|
||||
getSkillManager: vi.fn().mockReturnValue({ getSkills: () => [] }),
|
||||
modelConfigService: {
|
||||
registerRuntimeModelConfig: vi.fn(),
|
||||
},
|
||||
sandboxManager: undefined,
|
||||
} as unknown as Parameters<typeof startMemoryService>[0];
|
||||
|
||||
await startMemoryService(mockConfig);
|
||||
|
||||
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('my-new-skill'),
|
||||
);
|
||||
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('/memory inbox'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProcessedSessionIds', () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type ConversationRecord,
|
||||
} from './chatRecordingService.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { coreEvents } from '../utils/events.js';
|
||||
import { isNodeError } from '../utils/errors.js';
|
||||
import { FRONTMATTER_REGEX, parseFrontmatter } from '../skills/skillLoader.js';
|
||||
import { LocalAgentExecutor } from '../agents/local-executor.js';
|
||||
@@ -640,6 +641,11 @@ export async function startMemoryService(config: Config): Promise<void> {
|
||||
debugLogger.log(
|
||||
`[MemoryService] Completed in ${elapsed}s. Created ${skillsCreated.length} skill(s): ${skillsCreated.join(', ')}`,
|
||||
);
|
||||
const skillList = skillsCreated.join(', ');
|
||||
coreEvents.emitFeedback(
|
||||
'info',
|
||||
`${skillsCreated.length} new skill${skillsCreated.length > 1 ? 's' : ''} extracted from past sessions: ${skillList}. Use /memory inbox to review.`,
|
||||
);
|
||||
} else {
|
||||
debugLogger.log(
|
||||
`[MemoryService] Completed in ${elapsed}s. No new skills created (processed ${newSessionIds.length} session(s))`,
|
||||
|
||||
@@ -59,52 +59,56 @@ export class SandboxedFileSystemService implements FileSystemService {
|
||||
},
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Direct spawn is necessary here for streaming large file contents.
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
// Direct spawn is necessary here for streaming large file contents.
|
||||
|
||||
const child = spawn(prepared.program, prepared.args, {
|
||||
cwd: this.cwd,
|
||||
env: prepared.env,
|
||||
});
|
||||
const child = spawn(prepared.program, prepared.args, {
|
||||
cwd: this.cwd,
|
||||
env: prepared.env,
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let error = '';
|
||||
let output = '';
|
||||
let error = '';
|
||||
|
||||
child.stdout?.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
child.stdout?.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
child.stderr?.on('data', (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(output);
|
||||
} else {
|
||||
const isEnoent =
|
||||
error.toLowerCase().includes('no such file or directory') ||
|
||||
error.toLowerCase().includes('enoent') ||
|
||||
error.toLowerCase().includes('could not find file') ||
|
||||
error.toLowerCase().includes('could not find a part of the path');
|
||||
const err = new Error(
|
||||
`Sandbox Error: read_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`,
|
||||
);
|
||||
if (isEnoent) {
|
||||
Object.assign(err, { code: 'ENOENT' });
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(output);
|
||||
} else {
|
||||
const isEnoent =
|
||||
error.toLowerCase().includes('no such file or directory') ||
|
||||
error.toLowerCase().includes('enoent') ||
|
||||
error.toLowerCase().includes('could not find file') ||
|
||||
error.toLowerCase().includes('could not find a part of the path');
|
||||
const err = new Error(
|
||||
`Sandbox Error: read_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`,
|
||||
);
|
||||
if (isEnoent) {
|
||||
Object.assign(err, { code: 'ENOENT' });
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
reject(
|
||||
new Error(
|
||||
`Sandbox Error: Failed to spawn read_file for '${filePath}': ${err.message}`,
|
||||
),
|
||||
);
|
||||
child.on('error', (err) => {
|
||||
reject(
|
||||
new Error(
|
||||
`Sandbox Error: Failed to spawn read_file for '${filePath}': ${err.message}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
prepared.cleanup?.();
|
||||
}
|
||||
}
|
||||
|
||||
async writeTextFile(filePath: string, content: string): Promise<void> {
|
||||
@@ -124,53 +128,57 @@ export class SandboxedFileSystemService implements FileSystemService {
|
||||
},
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Direct spawn is necessary here for streaming large file contents.
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
// Direct spawn is necessary here for streaming large file contents.
|
||||
|
||||
const child = spawn(prepared.program, prepared.args, {
|
||||
cwd: this.cwd,
|
||||
env: prepared.env,
|
||||
});
|
||||
const child = spawn(prepared.program, prepared.args, {
|
||||
cwd: this.cwd,
|
||||
env: prepared.env,
|
||||
});
|
||||
|
||||
child.stdin?.on('error', (err) => {
|
||||
// Silently ignore EPIPE errors on stdin, they will be caught by the process error/close listeners
|
||||
if (isNodeError(err) && err.code === 'EPIPE') {
|
||||
return;
|
||||
}
|
||||
debugLogger.error(
|
||||
`Sandbox Error: stdin error for '${filePath}': ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
);
|
||||
});
|
||||
child.stdin?.on('error', (err) => {
|
||||
// Silently ignore EPIPE errors on stdin, they will be caught by the process error/close listeners
|
||||
if (isNodeError(err) && err.code === 'EPIPE') {
|
||||
return;
|
||||
}
|
||||
debugLogger.error(
|
||||
`Sandbox Error: stdin error for '${filePath}': ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
);
|
||||
});
|
||||
|
||||
child.stdin?.write(content);
|
||||
child.stdin?.end();
|
||||
child.stdin?.write(content);
|
||||
child.stdin?.end();
|
||||
|
||||
let error = '';
|
||||
child.stderr?.on('data', (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
let error = '';
|
||||
child.stderr?.on('data', (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Sandbox Error: write_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
reject(
|
||||
new Error(
|
||||
`Sandbox Error: write_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`,
|
||||
`Sandbox Error: Failed to spawn write_file for '${filePath}': ${err.message}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
reject(
|
||||
new Error(
|
||||
`Sandbox Error: Failed to spawn write_file for '${filePath}': ${err.message}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
prepared.cleanup?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,21 +510,24 @@ export class ShellExecutionService {
|
||||
shellExecutionConfig: ShellExecutionConfig,
|
||||
isInteractive: boolean,
|
||||
): Promise<ShellExecutionHandle> {
|
||||
let cmdCleanup: (() => void) | undefined;
|
||||
try {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
|
||||
const prepared = await this.prepareExecution(
|
||||
commandToExecute,
|
||||
cwd,
|
||||
shellExecutionConfig,
|
||||
isInteractive,
|
||||
);
|
||||
cmdCleanup = prepared.cleanup;
|
||||
|
||||
const {
|
||||
program: finalExecutable,
|
||||
args: finalArgs,
|
||||
env: finalEnv,
|
||||
cwd: finalCwd,
|
||||
cleanup: cmdCleanup,
|
||||
} = await this.prepareExecution(
|
||||
commandToExecute,
|
||||
cwd,
|
||||
shellExecutionConfig,
|
||||
isInteractive,
|
||||
);
|
||||
} = prepared;
|
||||
|
||||
const child = cpSpawn(finalExecutable, finalArgs, {
|
||||
cwd: finalCwd,
|
||||
@@ -811,6 +814,7 @@ export class ShellExecutionService {
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const error = e as Error;
|
||||
cmdCleanup?.();
|
||||
return {
|
||||
pid: undefined,
|
||||
result: Promise.resolve({
|
||||
@@ -826,7 +830,6 @@ export class ShellExecutionService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static async executeWithPty(
|
||||
commandToExecute: string,
|
||||
cwd: string,
|
||||
@@ -840,23 +843,26 @@ export class ShellExecutionService {
|
||||
throw new Error('PTY implementation not found');
|
||||
}
|
||||
let spawnedPty: IPty | undefined;
|
||||
let cmdCleanup: (() => void) | undefined;
|
||||
|
||||
try {
|
||||
const cols = shellExecutionConfig.terminalWidth ?? 80;
|
||||
const rows = shellExecutionConfig.terminalHeight ?? 30;
|
||||
|
||||
const prepared = await this.prepareExecution(
|
||||
commandToExecute,
|
||||
cwd,
|
||||
shellExecutionConfig,
|
||||
true,
|
||||
);
|
||||
cmdCleanup = prepared.cleanup;
|
||||
|
||||
const {
|
||||
program: finalExecutable,
|
||||
args: finalArgs,
|
||||
env: finalEnv,
|
||||
cwd: finalCwd,
|
||||
cleanup: cmdCleanup,
|
||||
} = await this.prepareExecution(
|
||||
commandToExecute,
|
||||
cwd,
|
||||
shellExecutionConfig,
|
||||
true,
|
||||
);
|
||||
} = prepared;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const ptyProcess = ptyInfo.module.spawn(finalExecutable, finalArgs, {
|
||||
@@ -1237,6 +1243,7 @@ export class ShellExecutionService {
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const error = e as Error;
|
||||
cmdCleanup?.();
|
||||
|
||||
if (spawnedPty) {
|
||||
try {
|
||||
@@ -1270,7 +1277,6 @@ export class ShellExecutionService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string to the pseudo-terminal (PTY) of a running process.
|
||||
*
|
||||
|
||||
@@ -326,6 +326,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
let finalCommand = checkCommand;
|
||||
let finalArgs = checkArgs;
|
||||
let finalEnv = process.env;
|
||||
let cleanup: (() => void) | undefined;
|
||||
|
||||
if (sandboxManager) {
|
||||
try {
|
||||
@@ -338,6 +339,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
finalCommand = prepared.program;
|
||||
finalArgs = prepared.args;
|
||||
finalEnv = prepared.env;
|
||||
cleanup = prepared.cleanup;
|
||||
} catch (err) {
|
||||
debugLogger.debug(
|
||||
`[GrepTool] Sandbox preparation failed for '${command}':`,
|
||||
@@ -346,21 +348,27 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
}
|
||||
}
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
const child = spawn(finalCommand, finalArgs, {
|
||||
stdio: 'ignore',
|
||||
shell: true,
|
||||
env: finalEnv,
|
||||
try {
|
||||
return await new Promise((resolve) => {
|
||||
const child = spawn(finalCommand, finalArgs, {
|
||||
stdio: 'ignore',
|
||||
shell: true,
|
||||
env: finalEnv,
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
resolve(code === 0);
|
||||
});
|
||||
child.on('error', (err) => {
|
||||
debugLogger.debug(
|
||||
`[GrepTool] Failed to start process for '${command}':`,
|
||||
err.message,
|
||||
);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
child.on('close', (code) => resolve(code === 0));
|
||||
child.on('error', (err) => {
|
||||
debugLogger.debug(
|
||||
`[GrepTool] Failed to start process for '${command}':`,
|
||||
err.message,
|
||||
);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
cleanup?.();
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ class DiscoveredToolInvocation extends BaseToolInvocation<
|
||||
let finalCommand = callCommand;
|
||||
let finalArgs = args;
|
||||
let finalEnv = process.env;
|
||||
let cleanupFunc: (() => void) | undefined;
|
||||
|
||||
const sandboxManager = this.config.sandboxManager;
|
||||
if (sandboxManager) {
|
||||
@@ -77,58 +78,63 @@ class DiscoveredToolInvocation extends BaseToolInvocation<
|
||||
finalCommand = prepared.program;
|
||||
finalArgs = prepared.args;
|
||||
finalEnv = prepared.env;
|
||||
cleanupFunc = prepared.cleanup;
|
||||
}
|
||||
|
||||
const child = spawn(finalCommand, finalArgs, {
|
||||
env: finalEnv,
|
||||
});
|
||||
child.stdin.write(JSON.stringify(this.params));
|
||||
child.stdin.end();
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let error: Error | null = null;
|
||||
let code: number | null = null;
|
||||
let signal: NodeJS.Signals | null = null;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const onStdout = (data: Buffer) => {
|
||||
stdout += data?.toString();
|
||||
};
|
||||
try {
|
||||
const child = spawn(finalCommand, finalArgs, {
|
||||
env: finalEnv,
|
||||
});
|
||||
child.stdin.write(JSON.stringify(this.params));
|
||||
child.stdin.end();
|
||||
|
||||
const onStderr = (data: Buffer) => {
|
||||
stderr += data?.toString();
|
||||
};
|
||||
await new Promise<void>((resolve) => {
|
||||
const onStdout = (data: Buffer) => {
|
||||
stdout += data?.toString();
|
||||
};
|
||||
|
||||
const onError = (err: Error) => {
|
||||
error = err;
|
||||
};
|
||||
const onStderr = (data: Buffer) => {
|
||||
stderr += data?.toString();
|
||||
};
|
||||
|
||||
const onClose = (
|
||||
_code: number | null,
|
||||
_signal: NodeJS.Signals | null,
|
||||
) => {
|
||||
code = _code;
|
||||
signal = _signal;
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const onError = (err: Error) => {
|
||||
error = err;
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
child.stdout.removeListener('data', onStdout);
|
||||
child.stderr.removeListener('data', onStderr);
|
||||
child.removeListener('error', onError);
|
||||
child.removeListener('close', onClose);
|
||||
if (child.connected) {
|
||||
child.disconnect();
|
||||
}
|
||||
};
|
||||
const onClose = (
|
||||
_code: number | null,
|
||||
_signal: NodeJS.Signals | null,
|
||||
) => {
|
||||
code = _code;
|
||||
signal = _signal;
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
|
||||
child.stdout.on('data', onStdout);
|
||||
child.stderr.on('data', onStderr);
|
||||
child.on('error', onError);
|
||||
child.on('close', onClose);
|
||||
});
|
||||
const cleanup = () => {
|
||||
child.stdout.removeListener('data', onStdout);
|
||||
child.stderr.removeListener('data', onStderr);
|
||||
child.removeListener('error', onError);
|
||||
child.removeListener('close', onClose);
|
||||
if (child.connected) {
|
||||
child.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
child.stdout.on('data', onStdout);
|
||||
child.stderr.on('data', onStderr);
|
||||
child.on('error', onError);
|
||||
child.on('close', onClose);
|
||||
});
|
||||
} finally {
|
||||
cleanupFunc?.();
|
||||
}
|
||||
|
||||
// if there is any error, non-zero exit code, signal, or stderr, return error details instead of stdout
|
||||
if (error || code !== 0 || signal || stderr) {
|
||||
@@ -374,6 +380,7 @@ export class ToolRegistry {
|
||||
.slice(1)
|
||||
.filter((p): p is string => typeof p === 'string');
|
||||
let finalEnv = process.env;
|
||||
let cleanupFunc: (() => void) | undefined;
|
||||
|
||||
const sandboxManager = this.config.sandboxManager;
|
||||
if (sandboxManager) {
|
||||
@@ -386,118 +393,127 @@ export class ToolRegistry {
|
||||
finalCommand = prepared.program;
|
||||
finalArgs = prepared.args;
|
||||
finalEnv = prepared.env;
|
||||
cleanupFunc = prepared.cleanup;
|
||||
}
|
||||
|
||||
const proc = spawn(finalCommand, finalArgs, {
|
||||
env: finalEnv,
|
||||
});
|
||||
let stdout = '';
|
||||
const stdoutDecoder = new StringDecoder('utf8');
|
||||
let stderr = '';
|
||||
const stderrDecoder = new StringDecoder('utf8');
|
||||
let sizeLimitExceeded = false;
|
||||
const MAX_STDOUT_SIZE = 10 * 1024 * 1024; // 10MB limit
|
||||
const MAX_STDERR_SIZE = 10 * 1024 * 1024; // 10MB limit
|
||||
|
||||
let stdoutByteLength = 0;
|
||||
let stderrByteLength = 0;
|
||||
|
||||
proc.stdout.on('data', (data) => {
|
||||
if (sizeLimitExceeded) return;
|
||||
if (stdoutByteLength + data.length > MAX_STDOUT_SIZE) {
|
||||
sizeLimitExceeded = true;
|
||||
proc.kill();
|
||||
return;
|
||||
}
|
||||
stdoutByteLength += data.length;
|
||||
stdout += stdoutDecoder.write(data);
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (data) => {
|
||||
if (sizeLimitExceeded) return;
|
||||
if (stderrByteLength + data.length > MAX_STDERR_SIZE) {
|
||||
sizeLimitExceeded = true;
|
||||
proc.kill();
|
||||
return;
|
||||
}
|
||||
stderrByteLength += data.length;
|
||||
stderr += stderrDecoder.write(data);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
proc.on('error', reject);
|
||||
proc.on('close', (code) => {
|
||||
stdout += stdoutDecoder.end();
|
||||
stderr += stderrDecoder.end();
|
||||
|
||||
if (sizeLimitExceeded) {
|
||||
return reject(
|
||||
new Error(
|
||||
`Tool discovery command output exceeded size limit of ${MAX_STDOUT_SIZE} bytes.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Tool discovery command failed with code ${code}.`,
|
||||
stderr,
|
||||
);
|
||||
return reject(
|
||||
new Error(`Tool discovery command failed with exit code ${code}`),
|
||||
);
|
||||
}
|
||||
resolve();
|
||||
try {
|
||||
const proc = spawn(finalCommand, finalArgs, {
|
||||
env: finalEnv,
|
||||
});
|
||||
});
|
||||
let stdout = '';
|
||||
const stdoutDecoder = new StringDecoder('utf8');
|
||||
let stderr = '';
|
||||
const stderrDecoder = new StringDecoder('utf8');
|
||||
let sizeLimitExceeded = false;
|
||||
const MAX_STDOUT_SIZE = 10 * 1024 * 1024; // 10MB limit
|
||||
const MAX_STDERR_SIZE = 10 * 1024 * 1024; // 10MB limit
|
||||
|
||||
// execute discovery command and extract function declarations (w/ or w/o "tool" wrappers)
|
||||
const functions: FunctionDeclaration[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const discoveredItems = JSON.parse(stdout.trim());
|
||||
let stdoutByteLength = 0;
|
||||
let stderrByteLength = 0;
|
||||
|
||||
if (!discoveredItems || !Array.isArray(discoveredItems)) {
|
||||
throw new Error(
|
||||
'Tool discovery command did not return a JSON array of tools.',
|
||||
);
|
||||
}
|
||||
proc.stdout.on('data', (data) => {
|
||||
if (sizeLimitExceeded) return;
|
||||
if (stdoutByteLength + data.length > MAX_STDOUT_SIZE) {
|
||||
sizeLimitExceeded = true;
|
||||
proc.kill();
|
||||
return;
|
||||
}
|
||||
stdoutByteLength += data.length;
|
||||
stdout += stdoutDecoder.write(data);
|
||||
});
|
||||
|
||||
for (const tool of discoveredItems) {
|
||||
if (tool && typeof tool === 'object') {
|
||||
if (Array.isArray(tool['function_declarations'])) {
|
||||
functions.push(...tool['function_declarations']);
|
||||
} else if (Array.isArray(tool['functionDeclarations'])) {
|
||||
functions.push(...tool['functionDeclarations']);
|
||||
} else if (tool['name']) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
functions.push(tool as FunctionDeclaration);
|
||||
proc.stderr.on('data', (data) => {
|
||||
if (sizeLimitExceeded) return;
|
||||
if (stderrByteLength + data.length > MAX_STDERR_SIZE) {
|
||||
sizeLimitExceeded = true;
|
||||
proc.kill();
|
||||
return;
|
||||
}
|
||||
stderrByteLength += data.length;
|
||||
stderr += stderrDecoder.write(data);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
proc.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
proc.on('close', (code) => {
|
||||
stdout += stdoutDecoder.end();
|
||||
stderr += stderrDecoder.end();
|
||||
|
||||
if (sizeLimitExceeded) {
|
||||
return reject(
|
||||
new Error(
|
||||
`Tool discovery command output exceeded size limit of ${MAX_STDOUT_SIZE} bytes.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Tool discovery command failed with code ${code}.`,
|
||||
stderr,
|
||||
);
|
||||
return reject(
|
||||
new Error(
|
||||
`Tool discovery command failed with exit code ${code}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// execute discovery command and extract function declarations (w/ or w/o "tool" wrappers)
|
||||
const functions: FunctionDeclaration[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const discoveredItems = JSON.parse(stdout.trim());
|
||||
|
||||
if (!discoveredItems || !Array.isArray(discoveredItems)) {
|
||||
throw new Error(
|
||||
'Tool discovery command did not return a JSON array of tools.',
|
||||
);
|
||||
}
|
||||
|
||||
for (const tool of discoveredItems) {
|
||||
if (tool && typeof tool === 'object') {
|
||||
if (Array.isArray(tool['function_declarations'])) {
|
||||
functions.push(...tool['function_declarations']);
|
||||
} else if (Array.isArray(tool['functionDeclarations'])) {
|
||||
functions.push(...tool['functionDeclarations']);
|
||||
} else if (tool['name']) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
functions.push(tool as FunctionDeclaration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// register each function as a tool
|
||||
for (const func of functions) {
|
||||
if (!func.name) {
|
||||
debugLogger.warn('Discovered a tool with no name. Skipping.');
|
||||
continue;
|
||||
// register each function as a tool
|
||||
for (const func of functions) {
|
||||
if (!func.name) {
|
||||
debugLogger.warn('Discovered a tool with no name. Skipping.');
|
||||
continue;
|
||||
}
|
||||
const parameters =
|
||||
func.parametersJsonSchema &&
|
||||
typeof func.parametersJsonSchema === 'object' &&
|
||||
!Array.isArray(func.parametersJsonSchema)
|
||||
? func.parametersJsonSchema
|
||||
: {};
|
||||
this.registerTool(
|
||||
new DiscoveredTool(
|
||||
this.config,
|
||||
func.name,
|
||||
DISCOVERED_TOOL_PREFIX + func.name,
|
||||
func.description ?? '',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
parameters as Record<string, unknown>,
|
||||
this.messageBus,
|
||||
),
|
||||
);
|
||||
}
|
||||
const parameters =
|
||||
func.parametersJsonSchema &&
|
||||
typeof func.parametersJsonSchema === 'object' &&
|
||||
!Array.isArray(func.parametersJsonSchema)
|
||||
? func.parametersJsonSchema
|
||||
: {};
|
||||
this.registerTool(
|
||||
new DiscoveredTool(
|
||||
this.config,
|
||||
func.name,
|
||||
DISCOVERED_TOOL_PREFIX + func.name,
|
||||
func.description ?? '',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
parameters as Record<string, unknown>,
|
||||
this.messageBus,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
cleanupFunc?.();
|
||||
}
|
||||
} catch (e) {
|
||||
debugLogger.error(`Tool discovery command "${discoveryCmd}" failed:`, e);
|
||||
|
||||
@@ -847,34 +847,40 @@ export const spawnAsync = async (
|
||||
|
||||
const { program: finalCommand, args: finalArgs, env: finalEnv } = prepared;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(finalCommand, finalArgs, {
|
||||
...options,
|
||||
env: finalEnv,
|
||||
});
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn(finalCommand, finalArgs, {
|
||||
...options,
|
||||
env: finalEnv,
|
||||
});
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${code}:\n${stderr}`));
|
||||
}
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
reject(
|
||||
new Error(`Command failed with exit code ${code}:\n${stderr}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
reject(err);
|
||||
child.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
prepared.cleanup?.();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -902,109 +908,115 @@ export async function* execStreaming(
|
||||
env: options?.env ?? process.env,
|
||||
});
|
||||
|
||||
const { program: finalCommand, args: finalArgs, env: finalEnv } = prepared;
|
||||
|
||||
const child = spawn(finalCommand, finalArgs, {
|
||||
...options,
|
||||
env: finalEnv,
|
||||
// ensure we don't open a window on windows if possible/relevant
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: child.stdout,
|
||||
terminal: false,
|
||||
});
|
||||
|
||||
const errorChunks: Buffer[] = [];
|
||||
let stderrTotalBytes = 0;
|
||||
const MAX_STDERR_BYTES = 20 * 1024; // 20KB limit
|
||||
|
||||
child.stderr.on('data', (chunk) => {
|
||||
if (stderrTotalBytes < MAX_STDERR_BYTES) {
|
||||
errorChunks.push(chunk);
|
||||
stderrTotalBytes += chunk.length;
|
||||
}
|
||||
});
|
||||
|
||||
let error: Error | null = null;
|
||||
child.on('error', (err) => {
|
||||
error = err;
|
||||
});
|
||||
|
||||
const onAbort = () => {
|
||||
// If manually aborted by signal, we kill immediately.
|
||||
if (!child.killed) child.kill();
|
||||
};
|
||||
|
||||
if (options?.signal?.aborted) {
|
||||
onAbort();
|
||||
} else {
|
||||
options?.signal?.addEventListener('abort', onAbort);
|
||||
}
|
||||
|
||||
let finished = false;
|
||||
try {
|
||||
for await (const line of rl) {
|
||||
if (options?.signal?.aborted) break;
|
||||
yield line;
|
||||
}
|
||||
finished = true;
|
||||
} finally {
|
||||
rl.close();
|
||||
options?.signal?.removeEventListener('abort', onAbort);
|
||||
const { program: finalCommand, args: finalArgs, env: finalEnv } = prepared;
|
||||
|
||||
// Ensure process is killed when the generator is closed (consumer breaks loop)
|
||||
let killedByGenerator = false;
|
||||
if (!finished && child.exitCode === null && !child.killed) {
|
||||
try {
|
||||
child.kill();
|
||||
} catch {
|
||||
// ignore error if process is already dead
|
||||
const child = spawn(finalCommand, finalArgs, {
|
||||
...options,
|
||||
env: finalEnv,
|
||||
// ensure we don't open a window on windows if possible/relevant
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: child.stdout,
|
||||
terminal: false,
|
||||
});
|
||||
|
||||
const errorChunks: Buffer[] = [];
|
||||
let stderrTotalBytes = 0;
|
||||
const MAX_STDERR_BYTES = 20 * 1024; // 20KB limit
|
||||
|
||||
child.stderr.on('data', (chunk) => {
|
||||
if (stderrTotalBytes < MAX_STDERR_BYTES) {
|
||||
errorChunks.push(chunk);
|
||||
stderrTotalBytes += chunk.length;
|
||||
}
|
||||
killedByGenerator = true;
|
||||
});
|
||||
|
||||
let error: Error | null = null;
|
||||
child.on('error', (err) => {
|
||||
error = err;
|
||||
});
|
||||
|
||||
const onAbort = () => {
|
||||
// If manually aborted by signal, we kill immediately.
|
||||
if (!child.killed) child.kill();
|
||||
};
|
||||
|
||||
if (options?.signal?.aborted) {
|
||||
onAbort();
|
||||
} else {
|
||||
options?.signal?.addEventListener('abort', onAbort);
|
||||
}
|
||||
|
||||
// Ensure we wait for the process to exit to check codes
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// If an error occurred before we got here (e.g. spawn failure), reject immediately.
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
let finished = false;
|
||||
try {
|
||||
for await (const line of rl) {
|
||||
if (options?.signal?.aborted) break;
|
||||
yield line;
|
||||
}
|
||||
finished = true;
|
||||
} finally {
|
||||
rl.close();
|
||||
options?.signal?.removeEventListener('abort', onAbort);
|
||||
|
||||
// Ensure process is killed when the generator is closed (consumer breaks loop)
|
||||
let killedByGenerator = false;
|
||||
if (!finished && child.exitCode === null && !child.killed) {
|
||||
try {
|
||||
child.kill();
|
||||
} catch {
|
||||
// ignore error if process is already dead
|
||||
}
|
||||
killedByGenerator = true;
|
||||
}
|
||||
|
||||
function checkExit(code: number | null) {
|
||||
// If we aborted or killed it manually, we treat it as success (stop waiting)
|
||||
if (options?.signal?.aborted || killedByGenerator) {
|
||||
resolve();
|
||||
// Ensure we wait for the process to exit to check codes
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// If an error occurred before we got here (e.g. spawn failure), reject immediately.
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const allowed = options?.allowedExitCodes ?? [0];
|
||||
if (code !== null && allowed.includes(code)) {
|
||||
resolve();
|
||||
} else {
|
||||
// If we have an accumulated error or explicit error event
|
||||
if (error) reject(error);
|
||||
else {
|
||||
const stderr = Buffer.concat(errorChunks).toString('utf8');
|
||||
const truncatedMsg =
|
||||
stderrTotalBytes >= MAX_STDERR_BYTES ? '...[truncated]' : '';
|
||||
reject(
|
||||
new Error(
|
||||
`Process exited with code ${code}: ${stderr}${truncatedMsg}`,
|
||||
),
|
||||
);
|
||||
function checkExit(code: number | null) {
|
||||
// If we aborted or killed it manually, we treat it as success (stop waiting)
|
||||
if (options?.signal?.aborted || killedByGenerator) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const allowed = options?.allowedExitCodes ?? [0];
|
||||
if (code !== null && allowed.includes(code)) {
|
||||
resolve();
|
||||
} else {
|
||||
// If we have an accumulated error or explicit error event
|
||||
if (error) reject(error);
|
||||
else {
|
||||
const stderr = Buffer.concat(errorChunks).toString('utf8');
|
||||
const truncatedMsg =
|
||||
stderrTotalBytes >= MAX_STDERR_BYTES ? '...[truncated]' : '';
|
||||
reject(
|
||||
new Error(
|
||||
`Process exited with code ${code}: ${stderr}${truncatedMsg}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (child.exitCode !== null) {
|
||||
checkExit(child.exitCode);
|
||||
} else {
|
||||
child.on('close', (code) => checkExit(code));
|
||||
child.on('error', (err) => reject(err));
|
||||
}
|
||||
});
|
||||
if (child.exitCode !== null) {
|
||||
checkExit(child.exitCode);
|
||||
} else {
|
||||
child.on('close', (code) => checkExit(code));
|
||||
child.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
prepared.cleanup?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"description": "Gemini CLI SDK",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"displayName": "Gemini CLI Companion",
|
||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
||||
"version": "0.39.0-nightly.20260408.e77b22e63",
|
||||
"publisher": "google",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
Reference in New Issue
Block a user