fix: add full Gemini toolset support for UI rendering and permissions (#273)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -252,14 +252,16 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// File edit previews: write/edit/multi_edit/replace/write_file
|
||||
// File edit previews: write/edit/multi_edit/replace/write_file/write_file_gemini
|
||||
if (
|
||||
(t === "write" ||
|
||||
t === "edit" ||
|
||||
t === "multiedit" ||
|
||||
t === "replace" ||
|
||||
t === "write_file" ||
|
||||
t === "writefile") &&
|
||||
t === "writefile" ||
|
||||
t === "write_file_gemini" ||
|
||||
t === "writefilegemini") &&
|
||||
parsedArgs
|
||||
) {
|
||||
try {
|
||||
@@ -269,7 +271,11 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
||||
if (precomputedDiff) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
{t === "write" || t === "write_file" || t === "writefile" ? (
|
||||
{t === "write" ||
|
||||
t === "write_file" ||
|
||||
t === "writefile" ||
|
||||
t === "write_file_gemini" ||
|
||||
t === "writefilegemini" ? (
|
||||
<AdvancedDiffRenderer
|
||||
precomputed={precomputedDiff}
|
||||
kind="write"
|
||||
@@ -307,7 +313,13 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
||||
}
|
||||
|
||||
// Fallback to non-precomputed rendering
|
||||
if (t === "write" || t === "write_file" || t === "writefile") {
|
||||
if (
|
||||
t === "write" ||
|
||||
t === "write_file" ||
|
||||
t === "writefile" ||
|
||||
t === "write_file_gemini" ||
|
||||
t === "writefilegemini"
|
||||
) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
<AdvancedDiffRenderer
|
||||
@@ -362,7 +374,9 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
||||
t === "edit" ||
|
||||
t === "multiedit" ||
|
||||
t === "replace" ||
|
||||
t === "write_file"
|
||||
t === "write_file" ||
|
||||
t === "write_file_gemini" ||
|
||||
t === "writefilegemini"
|
||||
) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
@@ -531,7 +545,13 @@ export const ApprovalDialog = memo(function ApprovalDialog({
|
||||
if (!parsedArgs || !approvalRequest) return null;
|
||||
|
||||
const toolName = approvalRequest.toolName.toLowerCase();
|
||||
if (toolName === "write") {
|
||||
if (
|
||||
toolName === "write" ||
|
||||
toolName === "write_file" ||
|
||||
toolName === "writefile" ||
|
||||
toolName === "write_file_gemini" ||
|
||||
toolName === "writefilegemini"
|
||||
) {
|
||||
const result = computeAdvancedDiff({
|
||||
kind: "write",
|
||||
filePath: parsedArgs.file_path as string,
|
||||
@@ -673,14 +693,20 @@ function getHeaderLabel(toolName: string): string {
|
||||
if (t === "updateplan") return "Plan update";
|
||||
// Gemini toolset (snake_case)
|
||||
if (t === "run_shell_command") return "Shell command";
|
||||
if (t === "read_file_gemini") return "Read File";
|
||||
if (t === "list_directory") return "List Directory";
|
||||
if (t === "glob_gemini") return "Find Files";
|
||||
if (t === "search_file_content") return "Search in Files";
|
||||
if (t === "write_file_gemini") return "Write File";
|
||||
if (t === "write_todos") return "Update Todos";
|
||||
if (t === "read_many_files") return "Read Multiple Files";
|
||||
// Gemini toolset (PascalCase → lowercased)
|
||||
if (t === "runshellcommand") return "Shell command";
|
||||
if (t === "readfilegemini") return "Read File";
|
||||
if (t === "listdirectory") return "List Directory";
|
||||
if (t === "globgemini") return "Find Files";
|
||||
if (t === "searchfilecontent") return "Search in Files";
|
||||
if (t === "writefilegemini") return "Write File";
|
||||
if (t === "writetodos") return "Update Todos";
|
||||
if (t === "readmanyfiles") return "Read Multiple Files";
|
||||
// Shared/additional tools
|
||||
|
||||
@@ -135,6 +135,7 @@ export const ToolCallMessage = memo(({ line }: { line: ToolCallLine }) => {
|
||||
const parsedArgs = JSON.parse(line.argsText);
|
||||
if (parsedArgs.todos && Array.isArray(parsedArgs.todos)) {
|
||||
// Convert todos to safe format for TodoRenderer
|
||||
// Note: Anthropic/Codex use "content", Gemini uses "description"
|
||||
const safeTodos = parsedArgs.todos.map((t: unknown, i: number) => {
|
||||
const rec = isRecord(t) ? t : {};
|
||||
const status: "pending" | "in_progress" | "completed" =
|
||||
@@ -144,8 +145,13 @@ export const ToolCallMessage = memo(({ line }: { line: ToolCallLine }) => {
|
||||
? "in_progress"
|
||||
: "pending";
|
||||
const id = typeof rec.id === "string" ? rec.id : String(i);
|
||||
// Handle both "content" (Anthropic/Codex) and "description" (Gemini) fields
|
||||
const content =
|
||||
typeof rec.content === "string" ? rec.content : JSON.stringify(t);
|
||||
typeof rec.content === "string"
|
||||
? rec.content
|
||||
: typeof rec.description === "string"
|
||||
? rec.description
|
||||
: JSON.stringify(t);
|
||||
const priority: "high" | "medium" | "low" | undefined =
|
||||
rec.priority === "high"
|
||||
? "high"
|
||||
|
||||
@@ -42,15 +42,21 @@ export function getDisplayToolName(rawName: string): string {
|
||||
|
||||
// Gemini toolset (snake_case)
|
||||
if (rawName === "run_shell_command") return "Shell";
|
||||
if (rawName === "read_file_gemini") return "Read";
|
||||
if (rawName === "list_directory") return "LS";
|
||||
if (rawName === "glob_gemini") return "Glob";
|
||||
if (rawName === "search_file_content") return "Grep";
|
||||
if (rawName === "write_file_gemini") return "Write";
|
||||
if (rawName === "write_todos") return "TODO";
|
||||
if (rawName === "read_many_files") return "Read Multiple";
|
||||
|
||||
// Gemini toolset (PascalCase)
|
||||
if (rawName === "RunShellCommand") return "Shell";
|
||||
if (rawName === "ReadFileGemini") return "Read";
|
||||
if (rawName === "ListDirectory") return "LS";
|
||||
if (rawName === "GlobGemini") return "Glob";
|
||||
if (rawName === "SearchFileContent") return "Grep";
|
||||
if (rawName === "WriteFileGemini") return "Write";
|
||||
if (rawName === "WriteTodos") return "TODO";
|
||||
if (rawName === "ReadManyFiles") return "Read Multiple";
|
||||
|
||||
|
||||
@@ -20,7 +20,30 @@ import type {
|
||||
/**
|
||||
* Tools that don't require approval within working directory
|
||||
*/
|
||||
const WORKING_DIRECTORY_TOOLS = ["Read", "Glob", "Grep"];
|
||||
const WORKING_DIRECTORY_TOOLS = [
|
||||
// Default/Anthropic toolset
|
||||
"Read",
|
||||
"Glob",
|
||||
"Grep",
|
||||
// Codex toolset
|
||||
"read_file",
|
||||
"ReadFile",
|
||||
"list_dir",
|
||||
"ListDir",
|
||||
"grep_files",
|
||||
"GrepFiles",
|
||||
// Gemini toolset
|
||||
"read_file_gemini",
|
||||
"ReadFileGemini",
|
||||
"glob_gemini",
|
||||
"GlobGemini",
|
||||
"list_directory",
|
||||
"ListDirectory",
|
||||
"search_file_content",
|
||||
"SearchFileContent",
|
||||
"read_many_files",
|
||||
"ReadManyFiles",
|
||||
];
|
||||
const READ_ONLY_SHELL_TOOLS = new Set([
|
||||
"Bash",
|
||||
"shell",
|
||||
@@ -244,13 +267,32 @@ function isWithinAllowedDirectories(
|
||||
*/
|
||||
function buildPermissionQuery(toolName: string, toolArgs: ToolArgs): string {
|
||||
switch (toolName) {
|
||||
// File tools: "ToolName(path/to/file)"
|
||||
case "Read":
|
||||
case "read_file":
|
||||
case "Write":
|
||||
case "Edit":
|
||||
case "Glob":
|
||||
case "Grep": {
|
||||
// File tools: "ToolName(path/to/file)"
|
||||
case "Grep":
|
||||
// Codex file tools
|
||||
case "read_file":
|
||||
case "ReadFile":
|
||||
case "list_dir":
|
||||
case "ListDir":
|
||||
case "grep_files":
|
||||
case "GrepFiles":
|
||||
// Gemini file tools
|
||||
case "read_file_gemini":
|
||||
case "ReadFileGemini":
|
||||
case "write_file_gemini":
|
||||
case "WriteFileGemini":
|
||||
case "glob_gemini":
|
||||
case "GlobGemini":
|
||||
case "list_directory":
|
||||
case "ListDirectory":
|
||||
case "search_file_content":
|
||||
case "SearchFileContent":
|
||||
case "read_many_files":
|
||||
case "ReadManyFiles": {
|
||||
const filePath = extractFilePath(toolArgs);
|
||||
return filePath ? `${toolName}(${filePath})` : toolName;
|
||||
}
|
||||
@@ -286,6 +328,38 @@ function extractShellCommand(toolArgs: ToolArgs): string | string[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* File tools that use glob matching for permissions
|
||||
*/
|
||||
const FILE_TOOLS = [
|
||||
// Default/Anthropic toolset
|
||||
"Read",
|
||||
"Write",
|
||||
"Edit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
// Codex toolset
|
||||
"read_file",
|
||||
"ReadFile",
|
||||
"list_dir",
|
||||
"ListDir",
|
||||
"grep_files",
|
||||
"GrepFiles",
|
||||
// Gemini toolset
|
||||
"read_file_gemini",
|
||||
"ReadFileGemini",
|
||||
"write_file_gemini",
|
||||
"WriteFileGemini",
|
||||
"glob_gemini",
|
||||
"GlobGemini",
|
||||
"list_directory",
|
||||
"ListDirectory",
|
||||
"search_file_content",
|
||||
"SearchFileContent",
|
||||
"read_many_files",
|
||||
"ReadManyFiles",
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if query matches a permission pattern
|
||||
*/
|
||||
@@ -296,17 +370,7 @@ function matchesPattern(
|
||||
workingDirectory: string,
|
||||
): boolean {
|
||||
// File tools use glob matching
|
||||
if (
|
||||
[
|
||||
"Read",
|
||||
"read_file",
|
||||
"Write",
|
||||
"Edit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"grep_files",
|
||||
].includes(toolName)
|
||||
) {
|
||||
if (FILE_TOOLS.includes(toolName)) {
|
||||
return matchesFilePattern(query, pattern, workingDirectory);
|
||||
}
|
||||
|
||||
@@ -350,12 +414,16 @@ function getDefaultDecision(toolName: string): PermissionDecision {
|
||||
"GrepFiles",
|
||||
"UpdatePlan",
|
||||
// Gemini toolset (snake_case) - tools that don't require approval
|
||||
"read_file_gemini",
|
||||
"list_directory",
|
||||
"glob_gemini",
|
||||
"search_file_content",
|
||||
"write_todos",
|
||||
"read_many_files",
|
||||
// Gemini toolset (PascalCase) - tools that don't require approval
|
||||
"ReadFileGemini",
|
||||
"ListDirectory",
|
||||
"GlobGemini",
|
||||
"SearchFileContent",
|
||||
"WriteTodos",
|
||||
"ReadManyFiles",
|
||||
|
||||
Reference in New Issue
Block a user