fix: consolidate approval rendering into ApprovalSwitch component (#541)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
353
src/cli/App.tsx
353
src/cli/App.tsx
@@ -78,6 +78,7 @@ import {
|
||||
import { AgentSelector } from "./components/AgentSelector";
|
||||
// ApprovalDialog removed - all approvals now render inline
|
||||
import { ApprovalPreview } from "./components/ApprovalPreview";
|
||||
import { ApprovalSwitch } from "./components/ApprovalSwitch";
|
||||
import { AssistantMessage } from "./components/AssistantMessageRich";
|
||||
import { BashCommandMessage } from "./components/BashCommandMessage";
|
||||
import { CommandMessage } from "./components/CommandMessage";
|
||||
@@ -87,12 +88,6 @@ import { colors } from "./components/colors";
|
||||
import { ErrorMessage } from "./components/ErrorMessageRich";
|
||||
import { FeedbackDialog } from "./components/FeedbackDialog";
|
||||
import { HelpDialog } from "./components/HelpDialog";
|
||||
import { InlineBashApproval } from "./components/InlineBashApproval";
|
||||
import { InlineEnterPlanModeApproval } from "./components/InlineEnterPlanModeApproval";
|
||||
import { InlineFileEditApproval } from "./components/InlineFileEditApproval";
|
||||
import { InlineGenericApproval } from "./components/InlineGenericApproval";
|
||||
import { InlineQuestionApproval } from "./components/InlineQuestionApproval";
|
||||
import { InlineTaskApproval } from "./components/InlineTaskApproval";
|
||||
import { Input } from "./components/InputRich";
|
||||
import { McpSelector } from "./components/McpSelector";
|
||||
import { MemoryViewer } from "./components/MemoryViewer";
|
||||
@@ -108,7 +103,6 @@ import { ResumeSelector } from "./components/ResumeSelector";
|
||||
import { formatUsageStats } from "./components/SessionStats";
|
||||
// InlinePlanApproval kept for easy rollback if needed
|
||||
// import { InlinePlanApproval } from "./components/InlinePlanApproval";
|
||||
import { StaticPlanApproval } from "./components/StaticPlanApproval";
|
||||
import { StatusMessage } from "./components/StatusMessage";
|
||||
import { SubagentGroupDisplay } from "./components/SubagentGroupDisplay";
|
||||
import { SubagentGroupStatic } from "./components/SubagentGroupStatic";
|
||||
@@ -7138,269 +7132,32 @@ Plan file path: ${planFilePath}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this tool call matches the current ExitPlanMode approval
|
||||
const isExitPlanModeApproval =
|
||||
ln.kind === "tool_call" &&
|
||||
currentApproval?.toolName === "ExitPlanMode" &&
|
||||
ln.toolCallId === currentApproval?.toolCallId;
|
||||
|
||||
// Check if this tool call matches a file edit/write/patch approval
|
||||
const isFileEditApproval =
|
||||
// Check if this tool call matches the current approval awaiting user input
|
||||
const matchesCurrentApproval =
|
||||
ln.kind === "tool_call" &&
|
||||
currentApproval &&
|
||||
(isFileEditTool(currentApproval.toolName) ||
|
||||
isFileWriteTool(currentApproval.toolName) ||
|
||||
isPatchTool(currentApproval.toolName)) &&
|
||||
ln.toolCallId === currentApproval.toolCallId;
|
||||
|
||||
// Check if this tool call matches a bash/shell approval
|
||||
const isBashApproval =
|
||||
ln.kind === "tool_call" &&
|
||||
currentApproval &&
|
||||
isShellTool(currentApproval.toolName) &&
|
||||
ln.toolCallId === currentApproval.toolCallId;
|
||||
|
||||
// Check if this tool call matches an EnterPlanMode approval
|
||||
const isEnterPlanModeApproval =
|
||||
ln.kind === "tool_call" &&
|
||||
currentApproval?.toolName === "EnterPlanMode" &&
|
||||
ln.toolCallId === currentApproval?.toolCallId;
|
||||
|
||||
// Check if this tool call matches an AskUserQuestion approval
|
||||
const isAskUserQuestionApproval =
|
||||
ln.kind === "tool_call" &&
|
||||
currentApproval?.toolName === "AskUserQuestion" &&
|
||||
ln.toolCallId === currentApproval?.toolCallId;
|
||||
|
||||
// Check if this tool call matches a Task tool approval
|
||||
const isTaskToolApproval =
|
||||
ln.kind === "tool_call" &&
|
||||
currentApproval &&
|
||||
isTaskTool(currentApproval.toolName) &&
|
||||
ln.toolCallId === currentApproval.toolCallId;
|
||||
|
||||
// Parse file edit info from approval args
|
||||
const getFileEditInfo = () => {
|
||||
if (!isFileEditApproval || !currentApproval) return null;
|
||||
try {
|
||||
const args = JSON.parse(
|
||||
currentApproval.toolArgs || "{}",
|
||||
);
|
||||
|
||||
// For patch tools, use the input field
|
||||
if (isPatchTool(currentApproval.toolName)) {
|
||||
return {
|
||||
toolName: currentApproval.toolName,
|
||||
filePath: "", // Patch can have multiple files
|
||||
patchInput: args.input as string | undefined,
|
||||
toolCallId: ln.toolCallId,
|
||||
};
|
||||
}
|
||||
|
||||
// For regular file edit/write tools
|
||||
return {
|
||||
toolName: currentApproval.toolName,
|
||||
filePath: String(args.file_path || ""),
|
||||
content: args.content as string | undefined,
|
||||
oldString: args.old_string as string | undefined,
|
||||
newString: args.new_string as string | undefined,
|
||||
replaceAll: args.replace_all as boolean | undefined,
|
||||
edits: args.edits as
|
||||
| Array<{
|
||||
old_string: string;
|
||||
new_string: string;
|
||||
replace_all?: boolean;
|
||||
}>
|
||||
| undefined,
|
||||
toolCallId: ln.toolCallId,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const fileEditInfo = getFileEditInfo();
|
||||
|
||||
// Parse bash info from approval args
|
||||
const getBashInfo = () => {
|
||||
if (!isBashApproval || !currentApproval) return null;
|
||||
try {
|
||||
const args = JSON.parse(
|
||||
currentApproval.toolArgs || "{}",
|
||||
);
|
||||
const t = currentApproval.toolName.toLowerCase();
|
||||
|
||||
// Handle different bash tool arg formats
|
||||
let command = "";
|
||||
let description = "";
|
||||
|
||||
if (t === "shell") {
|
||||
// Shell tool uses command array and justification
|
||||
const cmdVal = args.command;
|
||||
command = Array.isArray(cmdVal)
|
||||
? cmdVal.join(" ")
|
||||
: typeof cmdVal === "string"
|
||||
? cmdVal
|
||||
: "(no command)";
|
||||
description =
|
||||
typeof args.justification === "string"
|
||||
? args.justification
|
||||
: "";
|
||||
} else {
|
||||
// Bash/shell_command uses command string and description
|
||||
command =
|
||||
typeof args.command === "string"
|
||||
? args.command
|
||||
: "(no command)";
|
||||
description =
|
||||
typeof args.description === "string"
|
||||
? args.description
|
||||
: "";
|
||||
}
|
||||
|
||||
return {
|
||||
toolName: currentApproval.toolName,
|
||||
command,
|
||||
description,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const bashInfo = getBashInfo();
|
||||
|
||||
// Parse Task tool info from approval args
|
||||
const getTaskInfo = () => {
|
||||
if (!isTaskToolApproval || !currentApproval) return null;
|
||||
try {
|
||||
const args = JSON.parse(
|
||||
currentApproval.toolArgs || "{}",
|
||||
);
|
||||
return {
|
||||
subagentType:
|
||||
typeof args.subagent_type === "string"
|
||||
? args.subagent_type
|
||||
: "unknown",
|
||||
description:
|
||||
typeof args.description === "string"
|
||||
? args.description
|
||||
: "(no description)",
|
||||
prompt:
|
||||
typeof args.prompt === "string"
|
||||
? args.prompt
|
||||
: "(no prompt)",
|
||||
model:
|
||||
typeof args.model === "string"
|
||||
? args.model
|
||||
: undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const taskInfo = getTaskInfo();
|
||||
|
||||
return (
|
||||
<Box key={ln.id} flexDirection="column" marginTop={1}>
|
||||
{/* For ExitPlanMode awaiting approval: render StaticPlanApproval */}
|
||||
{/* Plan preview is eagerly committed to staticItems, so this only shows options */}
|
||||
{isExitPlanModeApproval ? (
|
||||
<StaticPlanApproval
|
||||
onApprove={() => handlePlanApprove(false)}
|
||||
onApproveAndAcceptEdits={() =>
|
||||
handlePlanApprove(true)
|
||||
}
|
||||
onKeepPlanning={handlePlanKeepPlanning}
|
||||
{matchesCurrentApproval ? (
|
||||
<ApprovalSwitch
|
||||
approval={currentApproval}
|
||||
onApprove={handleApproveCurrent}
|
||||
onApproveAlways={handleApproveAlways}
|
||||
onDeny={handleDenyCurrent}
|
||||
onCancel={handleCancelApprovals}
|
||||
isFocused={true}
|
||||
/>
|
||||
) : isFileEditApproval && fileEditInfo ? (
|
||||
<InlineFileEditApproval
|
||||
fileEdit={fileEditInfo}
|
||||
onPlanApprove={handlePlanApprove}
|
||||
onPlanKeepPlanning={handlePlanKeepPlanning}
|
||||
onQuestionSubmit={handleQuestionSubmit}
|
||||
onEnterPlanModeApprove={handleEnterPlanModeApprove}
|
||||
onEnterPlanModeReject={handleEnterPlanModeReject}
|
||||
precomputedDiff={
|
||||
ln.toolCallId
|
||||
? precomputedDiffsRef.current.get(ln.toolCallId)
|
||||
: undefined
|
||||
}
|
||||
allDiffs={precomputedDiffsRef.current}
|
||||
onApprove={(diffs) => handleApproveCurrent(diffs)}
|
||||
onApproveAlways={(scope, diffs) =>
|
||||
handleApproveAlways(scope, diffs)
|
||||
}
|
||||
onDeny={(reason) => handleDenyCurrent(reason)}
|
||||
onCancel={handleCancelApprovals}
|
||||
isFocused={true}
|
||||
approveAlwaysText={
|
||||
currentApprovalContext?.approveAlwaysText
|
||||
}
|
||||
allowPersistence={
|
||||
currentApprovalContext?.allowPersistence ?? true
|
||||
}
|
||||
/>
|
||||
) : isBashApproval && bashInfo ? (
|
||||
<InlineBashApproval
|
||||
bashInfo={bashInfo}
|
||||
onApprove={() => handleApproveCurrent()}
|
||||
onApproveAlways={(scope) =>
|
||||
handleApproveAlways(scope)
|
||||
}
|
||||
onDeny={(reason) => handleDenyCurrent(reason)}
|
||||
onCancel={handleCancelApprovals}
|
||||
isFocused={true}
|
||||
approveAlwaysText={
|
||||
currentApprovalContext?.approveAlwaysText
|
||||
}
|
||||
allowPersistence={
|
||||
currentApprovalContext?.allowPersistence ?? true
|
||||
}
|
||||
/>
|
||||
) : isEnterPlanModeApproval ? (
|
||||
<InlineEnterPlanModeApproval
|
||||
onApprove={handleEnterPlanModeApprove}
|
||||
onReject={handleEnterPlanModeReject}
|
||||
isFocused={true}
|
||||
/>
|
||||
) : isAskUserQuestionApproval ? (
|
||||
<InlineQuestionApproval
|
||||
questions={getQuestionsFromApproval(
|
||||
currentApproval,
|
||||
)}
|
||||
onSubmit={handleQuestionSubmit}
|
||||
onCancel={handleCancelApprovals}
|
||||
isFocused={true}
|
||||
/>
|
||||
) : isTaskToolApproval && taskInfo ? (
|
||||
<InlineTaskApproval
|
||||
taskInfo={taskInfo}
|
||||
onApprove={() => handleApproveCurrent()}
|
||||
onApproveAlways={(scope) =>
|
||||
handleApproveAlways(scope)
|
||||
}
|
||||
onDeny={(reason) => handleDenyCurrent(reason)}
|
||||
onCancel={handleCancelApprovals}
|
||||
isFocused={true}
|
||||
approveAlwaysText={
|
||||
currentApprovalContext?.approveAlwaysText
|
||||
}
|
||||
allowPersistence={
|
||||
currentApprovalContext?.allowPersistence ?? true
|
||||
}
|
||||
/>
|
||||
) : ln.kind === "tool_call" &&
|
||||
currentApproval &&
|
||||
ln.toolCallId === currentApproval.toolCallId ? (
|
||||
// Generic fallback for any other tool needing approval
|
||||
<InlineGenericApproval
|
||||
toolName={currentApproval.toolName}
|
||||
toolArgs={currentApproval.toolArgs}
|
||||
onApprove={() => handleApproveCurrent()}
|
||||
onApproveAlways={(scope) =>
|
||||
handleApproveAlways(scope)
|
||||
}
|
||||
onDeny={(reason) => handleDenyCurrent(reason)}
|
||||
onCancel={handleCancelApprovals}
|
||||
isFocused={true}
|
||||
approveAlwaysText={
|
||||
currentApprovalContext?.approveAlwaysText
|
||||
@@ -7465,68 +7222,26 @@ Plan file path: ${planFilePath}`;
|
||||
{/* Fallback approval UI when backfill is disabled (no liveItems) */}
|
||||
{liveItems.length === 0 && currentApproval && (
|
||||
<Box flexDirection="column">
|
||||
{isTaskTool(currentApproval.toolName) ? (
|
||||
<InlineTaskApproval
|
||||
taskInfo={(() => {
|
||||
try {
|
||||
const args = JSON.parse(
|
||||
currentApproval.toolArgs || "{}",
|
||||
);
|
||||
return {
|
||||
subagentType:
|
||||
typeof args.subagent_type === "string"
|
||||
? args.subagent_type
|
||||
: "unknown",
|
||||
description:
|
||||
typeof args.description === "string"
|
||||
? args.description
|
||||
: "(no description)",
|
||||
prompt:
|
||||
typeof args.prompt === "string"
|
||||
? args.prompt
|
||||
: "(no prompt)",
|
||||
model:
|
||||
typeof args.model === "string"
|
||||
? args.model
|
||||
: undefined,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
subagentType: "unknown",
|
||||
description: "(parse error)",
|
||||
prompt: "(parse error)",
|
||||
};
|
||||
}
|
||||
})()}
|
||||
onApprove={() => handleApproveCurrent()}
|
||||
onApproveAlways={(scope) => handleApproveAlways(scope)}
|
||||
onDeny={(reason) => handleDenyCurrent(reason)}
|
||||
onCancel={handleCancelApprovals}
|
||||
isFocused={true}
|
||||
approveAlwaysText={
|
||||
currentApprovalContext?.approveAlwaysText
|
||||
}
|
||||
allowPersistence={
|
||||
currentApprovalContext?.allowPersistence ?? true
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<InlineGenericApproval
|
||||
toolName={currentApproval.toolName}
|
||||
toolArgs={currentApproval.toolArgs}
|
||||
onApprove={() => handleApproveCurrent()}
|
||||
onApproveAlways={(scope) => handleApproveAlways(scope)}
|
||||
onDeny={(reason) => handleDenyCurrent(reason)}
|
||||
onCancel={handleCancelApprovals}
|
||||
isFocused={true}
|
||||
approveAlwaysText={
|
||||
currentApprovalContext?.approveAlwaysText
|
||||
}
|
||||
allowPersistence={
|
||||
currentApprovalContext?.allowPersistence ?? true
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ApprovalSwitch
|
||||
approval={currentApproval}
|
||||
onApprove={handleApproveCurrent}
|
||||
onApproveAlways={handleApproveAlways}
|
||||
onDeny={handleDenyCurrent}
|
||||
onCancel={handleCancelApprovals}
|
||||
onPlanApprove={handlePlanApprove}
|
||||
onPlanKeepPlanning={handlePlanKeepPlanning}
|
||||
onQuestionSubmit={handleQuestionSubmit}
|
||||
onEnterPlanModeApprove={handleEnterPlanModeApprove}
|
||||
onEnterPlanModeReject={handleEnterPlanModeReject}
|
||||
allDiffs={precomputedDiffsRef.current}
|
||||
isFocused={true}
|
||||
approveAlwaysText={
|
||||
currentApprovalContext?.approveAlwaysText
|
||||
}
|
||||
allowPersistence={
|
||||
currentApprovalContext?.allowPersistence ?? true
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
336
src/cli/components/ApprovalSwitch.tsx
Normal file
336
src/cli/components/ApprovalSwitch.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
import { memo } from "react";
|
||||
import type { AdvancedDiffSuccess } from "../helpers/diff";
|
||||
import type { ApprovalRequest } from "../helpers/stream";
|
||||
import {
|
||||
isFileEditTool,
|
||||
isFileWriteTool,
|
||||
isPatchTool,
|
||||
isShellTool,
|
||||
isTaskTool,
|
||||
} from "../helpers/toolNameMapping.js";
|
||||
import { InlineBashApproval } from "./InlineBashApproval";
|
||||
import { InlineEnterPlanModeApproval } from "./InlineEnterPlanModeApproval";
|
||||
import { InlineFileEditApproval } from "./InlineFileEditApproval";
|
||||
import { InlineGenericApproval } from "./InlineGenericApproval";
|
||||
import { InlineQuestionApproval } from "./InlineQuestionApproval";
|
||||
import { InlineTaskApproval } from "./InlineTaskApproval";
|
||||
import { StaticPlanApproval } from "./StaticPlanApproval";
|
||||
|
||||
// Types for parsed tool data
|
||||
type BashInfo = {
|
||||
toolName: string;
|
||||
command: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type FileEditInfo = {
|
||||
toolName: string;
|
||||
filePath: string;
|
||||
content?: string;
|
||||
oldString?: string;
|
||||
newString?: string;
|
||||
replaceAll?: boolean;
|
||||
edits?: Array<{
|
||||
old_string: string;
|
||||
new_string: string;
|
||||
replace_all?: boolean;
|
||||
}>;
|
||||
patchInput?: string;
|
||||
toolCallId?: string;
|
||||
};
|
||||
|
||||
type TaskInfo = {
|
||||
subagentType: string;
|
||||
description: string;
|
||||
prompt: string;
|
||||
model?: string;
|
||||
};
|
||||
|
||||
type Question = {
|
||||
question: string;
|
||||
header: string;
|
||||
options: Array<{ label: string; description: string }>;
|
||||
multiSelect: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
approval: ApprovalRequest;
|
||||
|
||||
// Common handlers
|
||||
onApprove: (diffs?: Map<string, AdvancedDiffSuccess>) => void;
|
||||
onApproveAlways: (
|
||||
scope: "project" | "session",
|
||||
diffs?: Map<string, AdvancedDiffSuccess>,
|
||||
) => void;
|
||||
onDeny: (reason: string) => void;
|
||||
onCancel?: () => void;
|
||||
isFocused?: boolean;
|
||||
approveAlwaysText?: string;
|
||||
allowPersistence?: boolean;
|
||||
|
||||
// Special handlers for ExitPlanMode
|
||||
onPlanApprove?: (acceptEdits: boolean) => void;
|
||||
onPlanKeepPlanning?: (reason: string) => void;
|
||||
|
||||
// Special handlers for AskUserQuestion
|
||||
onQuestionSubmit?: (answers: Record<string, string>) => void;
|
||||
|
||||
// Special handlers for EnterPlanMode
|
||||
onEnterPlanModeApprove?: () => void;
|
||||
onEnterPlanModeReject?: () => void;
|
||||
|
||||
// External data for FileEdit approvals
|
||||
precomputedDiff?: AdvancedDiffSuccess;
|
||||
allDiffs?: Map<string, AdvancedDiffSuccess>;
|
||||
};
|
||||
|
||||
// Parse bash info from approval args
|
||||
function getBashInfo(approval: ApprovalRequest): BashInfo | null {
|
||||
try {
|
||||
const args = JSON.parse(approval.toolArgs || "{}");
|
||||
const t = approval.toolName.toLowerCase();
|
||||
|
||||
let command = "";
|
||||
let description = "";
|
||||
|
||||
if (t === "shell") {
|
||||
// Shell tool uses command array and justification
|
||||
const cmdVal = args.command;
|
||||
command = Array.isArray(cmdVal)
|
||||
? cmdVal.join(" ")
|
||||
: typeof cmdVal === "string"
|
||||
? cmdVal
|
||||
: "(no command)";
|
||||
description =
|
||||
typeof args.justification === "string" ? args.justification : "";
|
||||
} else {
|
||||
// Bash/shell_command uses command string and description
|
||||
command =
|
||||
typeof args.command === "string" ? args.command : "(no command)";
|
||||
description =
|
||||
typeof args.description === "string" ? args.description : "";
|
||||
}
|
||||
|
||||
return {
|
||||
toolName: approval.toolName,
|
||||
command,
|
||||
description,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse file edit info from approval args
|
||||
function getFileEditInfo(approval: ApprovalRequest): FileEditInfo | null {
|
||||
try {
|
||||
const args = JSON.parse(approval.toolArgs || "{}");
|
||||
|
||||
// For patch tools, use the input field
|
||||
if (isPatchTool(approval.toolName)) {
|
||||
return {
|
||||
toolName: approval.toolName,
|
||||
filePath: "", // Patch can have multiple files
|
||||
patchInput: args.input as string | undefined,
|
||||
toolCallId: approval.toolCallId,
|
||||
};
|
||||
}
|
||||
|
||||
// For regular file edit/write tools
|
||||
return {
|
||||
toolName: approval.toolName,
|
||||
filePath: String(args.file_path || ""),
|
||||
content: args.content as string | undefined,
|
||||
oldString: args.old_string as string | undefined,
|
||||
newString: args.new_string as string | undefined,
|
||||
replaceAll: args.replace_all as boolean | undefined,
|
||||
edits: args.edits as FileEditInfo["edits"],
|
||||
toolCallId: approval.toolCallId,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse task info from approval args
|
||||
function getTaskInfo(approval: ApprovalRequest): TaskInfo | null {
|
||||
try {
|
||||
const args = JSON.parse(approval.toolArgs || "{}");
|
||||
return {
|
||||
subagentType:
|
||||
typeof args.subagent_type === "string" ? args.subagent_type : "unknown",
|
||||
description:
|
||||
typeof args.description === "string"
|
||||
? args.description
|
||||
: "(no description)",
|
||||
prompt: typeof args.prompt === "string" ? args.prompt : "(no prompt)",
|
||||
model: typeof args.model === "string" ? args.model : undefined,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
subagentType: "unknown",
|
||||
description: "(parse error)",
|
||||
prompt: "(parse error)",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Parse questions from AskUserQuestion args
|
||||
function getQuestions(approval: ApprovalRequest): Question[] {
|
||||
try {
|
||||
const args = JSON.parse(approval.toolArgs || "{}");
|
||||
return (args.questions as Question[]) || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ApprovalSwitch - Unified approval component that renders the appropriate
|
||||
* specialized approval UI based on tool type.
|
||||
*
|
||||
* This consolidates the approval rendering logic that was previously duplicated
|
||||
* in the transcript rendering and fallback UI paths.
|
||||
*/
|
||||
export const ApprovalSwitch = memo(
|
||||
({
|
||||
approval,
|
||||
onApprove,
|
||||
onApproveAlways,
|
||||
onDeny,
|
||||
onCancel,
|
||||
isFocused = true,
|
||||
approveAlwaysText,
|
||||
allowPersistence = true,
|
||||
onPlanApprove,
|
||||
onPlanKeepPlanning,
|
||||
onQuestionSubmit,
|
||||
onEnterPlanModeApprove,
|
||||
onEnterPlanModeReject,
|
||||
precomputedDiff,
|
||||
allDiffs,
|
||||
}: Props) => {
|
||||
const toolName = approval.toolName;
|
||||
|
||||
// 1. ExitPlanMode → StaticPlanApproval
|
||||
if (toolName === "ExitPlanMode" && onPlanApprove && onPlanKeepPlanning) {
|
||||
return (
|
||||
<StaticPlanApproval
|
||||
onApprove={() => onPlanApprove(false)}
|
||||
onApproveAndAcceptEdits={() => onPlanApprove(true)}
|
||||
onKeepPlanning={onPlanKeepPlanning}
|
||||
onCancel={onCancel ?? (() => {})}
|
||||
isFocused={isFocused}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 2. File edit/write/patch tools → InlineFileEditApproval
|
||||
if (
|
||||
isFileEditTool(toolName) ||
|
||||
isFileWriteTool(toolName) ||
|
||||
isPatchTool(toolName)
|
||||
) {
|
||||
const fileEditInfo = getFileEditInfo(approval);
|
||||
if (fileEditInfo) {
|
||||
return (
|
||||
<InlineFileEditApproval
|
||||
fileEdit={fileEditInfo}
|
||||
precomputedDiff={precomputedDiff}
|
||||
allDiffs={allDiffs}
|
||||
onApprove={(diffs) => onApprove(diffs)}
|
||||
onApproveAlways={(scope, diffs) => onApproveAlways(scope, diffs)}
|
||||
onDeny={onDeny}
|
||||
onCancel={onCancel}
|
||||
isFocused={isFocused}
|
||||
approveAlwaysText={approveAlwaysText}
|
||||
allowPersistence={allowPersistence}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Shell/Bash tools → InlineBashApproval
|
||||
if (isShellTool(toolName)) {
|
||||
const bashInfo = getBashInfo(approval);
|
||||
if (bashInfo) {
|
||||
return (
|
||||
<InlineBashApproval
|
||||
bashInfo={bashInfo}
|
||||
onApprove={() => onApprove()}
|
||||
onApproveAlways={(scope) => onApproveAlways(scope)}
|
||||
onDeny={onDeny}
|
||||
onCancel={onCancel}
|
||||
isFocused={isFocused}
|
||||
approveAlwaysText={approveAlwaysText}
|
||||
allowPersistence={allowPersistence}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. EnterPlanMode → InlineEnterPlanModeApproval
|
||||
if (
|
||||
toolName === "EnterPlanMode" &&
|
||||
onEnterPlanModeApprove &&
|
||||
onEnterPlanModeReject
|
||||
) {
|
||||
return (
|
||||
<InlineEnterPlanModeApproval
|
||||
onApprove={onEnterPlanModeApprove}
|
||||
onReject={onEnterPlanModeReject}
|
||||
isFocused={isFocused}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 5. AskUserQuestion → InlineQuestionApproval
|
||||
if (toolName === "AskUserQuestion" && onQuestionSubmit) {
|
||||
const questions = getQuestions(approval);
|
||||
return (
|
||||
<InlineQuestionApproval
|
||||
questions={questions}
|
||||
onSubmit={onQuestionSubmit}
|
||||
onCancel={onCancel}
|
||||
isFocused={isFocused}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Task tool → InlineTaskApproval
|
||||
if (isTaskTool(toolName)) {
|
||||
const taskInfo = getTaskInfo(approval);
|
||||
if (taskInfo) {
|
||||
return (
|
||||
<InlineTaskApproval
|
||||
taskInfo={taskInfo}
|
||||
onApprove={() => onApprove()}
|
||||
onApproveAlways={(scope) => onApproveAlways(scope)}
|
||||
onDeny={onDeny}
|
||||
onCancel={onCancel}
|
||||
isFocused={isFocused}
|
||||
approveAlwaysText={approveAlwaysText}
|
||||
allowPersistence={allowPersistence}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Fallback → InlineGenericApproval
|
||||
return (
|
||||
<InlineGenericApproval
|
||||
toolName={toolName}
|
||||
toolArgs={approval.toolArgs}
|
||||
onApprove={() => onApprove()}
|
||||
onApproveAlways={(scope) => onApproveAlways(scope)}
|
||||
onDeny={onDeny}
|
||||
onCancel={onCancel}
|
||||
isFocused={isFocused}
|
||||
approveAlwaysText={approveAlwaysText}
|
||||
allowPersistence={allowPersistence}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ApprovalSwitch.displayName = "ApprovalSwitch";
|
||||
Reference in New Issue
Block a user