From 93bccd1db9efb9484d7b4d3174d20bb2dfba5a45 Mon Sep 17 00:00:00 2001 From: AI Bot Date: Tue, 14 Jul 2026 00:21:48 +0530 Subject: [PATCH] fix(core): remove synchronous I/O from shell tool critical path This fixes the UI stutter issue #28395 where React Ink terminal frames were being paused due to synchronous fs calls like mkdtempSync and existsSync. --- packages/core/src/tools/shell.ts | 84 ++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 9ad657febdf..6ac61f3f7a0 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -5,7 +5,7 @@ */ import fsPromises from 'node:fs/promises'; -import fs from 'node:fs'; + import path from 'node:path'; import os from 'node:os'; import { debugLogger } from '../index.js'; @@ -498,7 +498,9 @@ export class ShellToolInvocation extends BaseToolInvocation< const onAbort = () => combinedController.abort(); try { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-shell-')); + tempDir = await fsPromises.mkdtemp( + path.join(os.tmpdir(), 'gemini-shell-'), + ); tempFilePath = path.join(tempDir, 'bgpids.tmp'); // Windows shells do not support the POSIX jobs output used here. @@ -906,45 +908,55 @@ export class ShellToolInvocation extends BaseToolInvocation< } if (sandboxDenial.filePaths) { - for (const p of sandboxDenial.filePaths) { - try { - // Find an existing parent directory to add instead of a non-existent file - let currentPath = p; - if (currentPath.startsWith('~')) { - currentPath = path.join(os.homedir(), currentPath.slice(1)); - } + await Promise.all( + sandboxDenial.filePaths.map(async (p) => { try { - if ( - fs.existsSync(currentPath) && - fs.statSync(currentPath).isFile() - ) { - currentPath = path.dirname(currentPath); + // Find an existing parent directory to add instead of a non-existent file + let currentPath = p; + if (currentPath.startsWith('~')) { + currentPath = path.join(os.homedir(), currentPath.slice(1)); } - } catch { - /* ignore */ - } - while (currentPath.length > 1) { - if (fs.existsSync(currentPath)) { - const mode = this.context.config.getApprovalMode(); - const isReadonlyMode = - this.context.config.sandboxPolicyManager.getModeConfig( - mode, - )?.readonly ?? false; - const isAllowed = - this.context.config.isPathAllowed(currentPath); - - if (!isAllowed || isReadonlyMode) { - writePaths.add(currentPath); - readPaths.add(currentPath); + try { + const exists = await fsPromises + .access(currentPath) + .then(() => true) + .catch(() => false); + if (exists) { + const stat = await fsPromises.stat(currentPath); + if (stat.isFile()) { + currentPath = path.dirname(currentPath); + } } - break; + } catch { + /* ignore */ } - currentPath = path.dirname(currentPath); + while (currentPath.length > 1) { + const exists = await fsPromises + .access(currentPath) + .then(() => true) + .catch(() => false); + if (exists) { + const mode = this.context.config.getApprovalMode(); + const isReadonlyMode = + this.context.config.sandboxPolicyManager.getModeConfig( + mode, + )?.readonly ?? false; + const isAllowed = + this.context.config.isPathAllowed(currentPath); + + if (!isAllowed || isReadonlyMode) { + writePaths.add(currentPath); + readPaths.add(currentPath); + } + break; + } + currentPath = path.dirname(currentPath); + } + } catch { + // ignore } - } catch { - // ignore - } - } + }), + ); } const simplifiedRead = this.simplifyPaths(readPaths);