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;
}