import type { JSX } from "react"; import type { AgentApprovalItemVM } from "../contracts/agentApprovalItemVM"; import { translate } from "../../../i18n/index"; import { Spinner, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@tutti-os/ui-system"; import { normalizeApprovalOptionToken } from "../approvalOptionPresentation"; import { getPromptToolDetails, type PromptToolDetail } from "../promptToolDetails"; import styles from "../../../agent-gui/agentGuiNode/AgentGUIConversation.styles"; const COMMAND_TOOLTIP_DELAY_MS = 1000; export function isEnterLikeKey(event: KeyboardEvent): boolean { return ( event.key === "Enter" || event.code === "Enter" || event.code === "NumpadEnter" ); } export function isEditableKeyboardTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) { return false; } const tagName = target.tagName.toLowerCase(); return ( tagName === "input" || tagName === "textarea" || tagName === "select" || target.isContentEditable ); } export function approvalOptionShortcutLabel( optionIndex: number, isDarwin: boolean ): string | null { if (optionIndex === 0) { return translate("agentHost.agentGui.shortcutEnter"); } if (optionIndex === 1) { return isDarwin ? translate("agentHost.agentGui.shortcutCmdEnter") : translate("agentHost.agentGui.shortcutCtrEnter"); } return null; } export function isDarwinPlatform(platform: string | undefined): boolean { if (platform) { return platform === "darwin"; } if (typeof navigator === "undefined") { return false; } const userAgentPlatform = "userAgentData" in navigator ? ( navigator as Navigator & { userAgentData?: { platform?: string }; } ).userAgentData?.platform : undefined; const navigatorPlatform = userAgentPlatform ?? navigator.platform ?? ""; return /mac/i.test(navigatorPlatform); } export function InteractiveOptionSpinner(): JSX.Element { "use memo"; return ( ); } export function SendFilledIcon(): JSX.Element { "use memo"; return ( ); } export function interactivePromptClassName(embedded: boolean): string { return embedded ? `${styles.interactivePrompt} agent-gui-conversation__interactive-prompt--embedded` : styles.interactivePrompt; } export function interactivePromptCardClassName(edgeGlow: boolean): string { return edgeGlow ? `${styles.interactivePromptCard} agent-gui-edge-glow` : styles.interactivePromptCard; } export interface LabeledPromptToolDetail { kind: PromptToolDetail["kind"]; label: string; value: string; meta?: string; } export interface ApprovalPromptToolPresentation { lead: string; leadDetails: LabeledPromptToolDetail[]; cardDetails: LabeledPromptToolDetail[]; } export function formatToolDetails( input: Record | null ): LabeledPromptToolDetail[] { return getPromptToolDetails(input).map((detail) => ({ kind: detail.kind, label: promptToolDetailLabel(detail.kind), value: detail.value, ...(detail.meta ? { meta: detail.meta } : {}) })); } export function formatApprovalToolPresentation( prompt: Pick, labels: { approvalLead: string; fileChangeApprovalLead: string } ): ApprovalPromptToolPresentation { const details = formatToolDetails(prompt.input); const hasFileChanges = details.some((detail) => detail.kind === "files"); const isLeadDetail = (detail: LabeledPromptToolDetail): boolean => detail.kind === "reason" || (hasFileChanges && (detail.kind === "directory" || detail.kind === "path")); const leadDetails = [ ...details.filter((detail) => detail.kind === "reason"), ...details.filter( (detail) => detail.kind !== "reason" && isLeadDetail(detail) ) ].filter( (detail, index, candidates) => candidates.findIndex( (candidate) => candidate.value === detail.value && candidate.meta === detail.meta ) === index ); const purposeLeads = { generic: labels.approvalLead, "edit-files": labels.fileChangeApprovalLead } satisfies Record< NonNullable | "generic", string >; return { lead: purposeLeads[prompt.approvalPurpose ?? "generic"], leadDetails, cardDetails: details.filter((detail) => !isLeadDetail(detail)) }; } export function PromptDetailValue({ detail }: { detail: LabeledPromptToolDetail; }): JSX.Element { "use memo"; if (detail.kind !== "command") { return ( {detail.value} ); } return ( ); } export function CommandTextWithTooltip({ value, testId, tooltipsEnabled = true }: { value: string; testId: string; tooltipsEnabled?: boolean; }): JSX.Element { "use memo"; const content = ( {value} ); if (!tooltipsEnabled) { return content; } return ( {content} {value} ); } export function promptToolDetailLabel(kind: PromptToolDetail["kind"]): string { switch (kind) { case "command": return translate("agentHost.agentTool.details.command"); case "directory": return translate("agentHost.agentTool.details.scope"); case "files": return translate("workspaceCanvas.nodeDockLabel.files"); case "mcp": return translate("agentHost.agentTool.details.mcp"); case "path": return translate("agentHost.agentTool.details.path"); case "prompt": return translate("agentHost.agentTool.details.prompt"); case "query": return translate("agentHost.agentTool.details.query"); case "reason": return translate("agentHost.agentTool.details.summary"); case "url": return translate("agentHost.agentTool.details.url"); } } export function isApprovalFeedbackOption(option: { id: string; kind: string; }): boolean { return ( isDenyApprovalOptionToken(option.id) || isDenyApprovalOptionToken(option.kind) ); } export function approvalFeedbackOptionId( options: readonly { id: string; kind: string }[] ): string | null { const explicitFeedbackOption = options.find((option) => isExplicitFeedbackDenyApprovalOption(option) ); if (explicitFeedbackOption) { return explicitFeedbackOption.id; } return options.find(isApprovalFeedbackOption)?.id ?? null; } export function isExplicitFeedbackDenyApprovalOption(option: { id: string; kind: string; }): boolean { for (const value of [option.id, option.kind]) { switch (normalizeApprovalOptionToken(value ?? "")) { case "denywithfeedback": case "rejectwithfeedback": return true; default: break; } } return false; } export function isDenyApprovalOptionToken( value: string | null | undefined ): boolean { switch (normalizeApprovalOptionToken(value ?? "")) { case "abort": case "cancel": case "cancelled": case "canceled": case "deny": case "denied": case "reject": case "rejected": case "rejectonce": case "disallow": case "decline": case "declined": case "no": return true; default: return false; } } export function stripPromptTitlePunctuation(value: string): string { return value.trim().replace(/[.。]+$/u, ""); } export function interactiveOptionLabel( label: string, description: string | null | undefined ): string { const trimmedDescription = description?.trim(); return trimmedDescription ? `${label} ${trimmedDescription}` : label; }