wip: dynamic expansion proto

This commit is contained in:
galz10
2026-03-17 18:23:00 -07:00
parent 7158876d77
commit e8d292cdb9
15 changed files with 609 additions and 410 deletions
+91 -1
View File
@@ -19,6 +19,8 @@ import {
useStdout,
useStdin,
type AppProps,
Box,
Text,
} from 'ink';
import { App } from './App.js';
import { AppContext } from './contexts/AppContext.js';
@@ -39,7 +41,7 @@ import {
} from './types.js';
import { checkPermissions } from './hooks/atCommandProcessor.js';
import { MessageType, StreamingState } from './types.js';
import { ToolActionsProvider } from './contexts/ToolActionsContext.js';
import { ToolActionsProvider, ToolActionsContext } from './contexts/ToolActionsContext.js';
import {
type StartupWarning,
type EditorType,
@@ -86,6 +88,7 @@ import {
logBillingEvent,
ApiKeyUpdatedEvent,
MessageBusType,
ToolConfirmationOutcome,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import process from 'node:process';
@@ -150,6 +153,9 @@ import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
import { useSettings } from './contexts/SettingsContext.js';
import { AskUserDialog } from './components/AskUserDialog.js';
import { ToolConfirmationMessage } from './components/messages/ToolConfirmationMessage.js';
import { StickyHeader } from './components/StickyHeader.js';
import { theme } from './semantic-colors.js';
import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js';
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
import { useBanner } from './hooks/useBanner.js';
@@ -1583,11 +1589,95 @@ Logging in with Google... Restarting Gemini CLI to continue.
}}
/>
);
} else if (msg.type === MessageBusType.SANDBOX_EXPANSION_REQUEST) {
const customToolActions = {
confirm: async (callId: string, outcome: ToolConfirmationOutcome) => {
messageBus.publish({
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId: msg.correlationId,
confirmed: outcome !== ToolConfirmationOutcome.Cancel,
outcome,
});
setCustomDialog(null);
},
cancel: async (callId: string) => {
messageBus.publish({
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId: msg.correlationId,
confirmed: false,
outcome: ToolConfirmationOutcome.Cancel,
});
setCustomDialog(null);
},
isDiffingEnabled: false,
};
setCustomDialog(
<Box flexDirection="column" width={terminalWidth} flexShrink={0}>
<StickyHeader
width={terminalWidth}
isFirst={true}
borderColor={theme.status.warning}
borderDimColor={false}
>
<Box flexDirection="column" width={terminalWidth - 4}>
<Box
marginBottom={1}
justifyContent="space-between"
>
<Text color={theme.status.warning} bold>
Action Required
</Text>
</Box>
</Box>
</StickyHeader>
<Box
width={terminalWidth}
borderStyle="round"
borderColor={theme.status.warning}
borderTop={false}
borderBottom={false}
borderLeft={true}
borderRight={true}
paddingX={1}
flexDirection="column"
>
<ToolActionsContext.Provider value={customToolActions}>
<ToolConfirmationMessage
callId={msg.correlationId}
confirmationDetails={{
type: 'sandbox_expansion',
title: msg.requiresUnsandboxed ? 'Unrestricted Execution Required' : 'Sandbox Violation',
blockedPath: msg.requiresUnsandboxed ? `${msg.blockedPath} (Requires Unsandboxed Execution)` : msg.blockedPath,
}} config={config}
getPreferredEditor={() => undefined}
terminalWidth={terminalWidth - 4}
availableTerminalHeight={terminalHeight}
isFocused={true}
/>
</ToolActionsContext.Provider>
</Box>
<Box
height={1}
width={terminalWidth}
borderLeft={true}
borderRight={true}
borderTop={false}
borderBottom={true}
borderStyle="round"
borderColor={theme.status.warning}
borderDimColor={false}
/>
</Box>
);
}
};
messageBus.subscribe(MessageBusType.ASK_USER_REQUEST, handler);
messageBus.subscribe(MessageBusType.SANDBOX_EXPANSION_REQUEST, handler);
return () => {
messageBus.unsubscribe(MessageBusType.ASK_USER_REQUEST, handler);
messageBus.unsubscribe(MessageBusType.SANDBOX_EXPANSION_REQUEST, handler);
};
}, [config, setCustomDialog, terminalWidth, terminalHeight]);
@@ -363,6 +363,24 @@ export const ToolConfirmationMessage: React.FC<
value: ToolConfirmationOutcome.Cancel,
key: 'No, suggest changes (esc)',
});
} else if (confirmationDetails.type === 'sandbox_expansion') {
options.push({
label: 'Allow once',
value: ToolConfirmationOutcome.ProceedOnce,
key: 'Allow once',
});
if (allowPermanentApproval) {
options.push({
label: 'Allow for all future sessions',
value: ToolConfirmationOutcome.ProceedAlwaysAndSave,
key: 'Allow for all future sessions',
});
}
options.push({
label: 'No, suggest changes (esc)',
value: ToolConfirmationOutcome.Cancel,
key: 'No, suggest changes (esc)',
});
}
return options;
}, [
@@ -484,6 +502,8 @@ export const ToolConfirmationMessage: React.FC<
// mcp tool confirmation
const mcpProps = confirmationDetails;
question = `Allow execution of MCP tool "${sanitizeForDisplay(mcpProps.toolName)}" from server "${sanitizeForDisplay(mcpProps.serverName)}"?`;
} else if (confirmationDetails.type === 'sandbox_expansion') {
question = `Sandbox blocked access to ${sanitizeForDisplay(confirmationDetails.blockedPath)}. Allow access?`;
}
if (confirmationDetails.type === 'edit') {
@@ -642,6 +662,17 @@ export const ToolConfirmationMessage: React.FC<
)}
</Box>
);
} else if (confirmationDetails.type === 'sandbox_expansion') {
bodyContent = (
<Box flexDirection="column">
<Text color={theme.text.primary}>
The sandbox prevented a command from accessing a file outside the workspace.
</Text>
<Box marginTop={1}>
<Text color={theme.text.link}>{sanitizeForDisplay(confirmationDetails.blockedPath)}</Text>
</Box>
</Box>
);
}
return { question, bodyContent, options, securityWarnings };
@@ -50,7 +50,7 @@ interface ToolActionsContextValue {
isDiffingEnabled: boolean;
}
const ToolActionsContext = createContext<ToolActionsContextValue | null>(null);
export const ToolActionsContext = createContext<ToolActionsContextValue | null>(null);
export const useToolActions = () => {
const context = useContext(ToolActionsContext);