import { APP_ACTION_MENU_CONTENT_CLASS } from "@agent-native/core/client/chat-first"; import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { ShareButton } from "@agent-native/core/client/sharing"; import { IconChevronDown, IconChevronRight, IconClockHour4, IconEdit, IconEye, IconEyeOff, IconFileText, IconKey, IconPin, IconShare3, IconSettings, IconTrash, IconUser, IconUsersGroup, IconWorld, } from "@tabler/icons-react"; import { useEffect, useRef, useState, type FormEvent } from "react"; import { useNavigate } from "react-router"; import { toast } from "sonner"; import { cn } from "../lib/utils"; import { isPathMountedWorkspaceApp, isWorkspaceSsoApp, navigateToWorkspaceApp, workspaceAppDirectHref, workspaceAppHref, workspaceAppRoute, type WorkspaceAppSummary, } from "../lib/workspace-apps"; import { ActionQueryError } from "./action-query-error"; import { AppIcon } from "./app-icon"; import { AppKeysPanel } from "./app-keys-popover"; import { AppListRow } from "./app-list-row"; import { AppOpenActions } from "./app-open-actions"; import { Badge } from "./ui/badge"; import { Button } from "./ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "./ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./ui/dropdown-menu"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; import { Textarea } from "./ui/textarea"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { AppResourceEffectiveStack } from "./workspace-resource-effective-stack"; const APP_CARD_ACTION_CLASS = "size-7 rounded-md p-0 text-muted-foreground transition-[background-color,color] hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:bg-accent data-[state=open]:text-foreground"; function deferWorkspaceAppOverlayOpen( event: { preventDefault: () => void }, closeMenu: () => void, openOverlay: () => void, ): void { event.preventDefault(); closeMenu(); if ( typeof window !== "undefined" && typeof window.requestAnimationFrame === "function" ) { window.requestAnimationFrame(() => openOverlay()); } else { setTimeout(openOverlay, 0); } } // The settings menu's own DropdownMenuContent restores focus to its trigger // once its FocusScope unmounts, independent of the requestAnimationFrame // above. If that restore lands after the deferred overlay's DismissableLayer // has already mounted, the resulting focusin event reads as an outside // interaction and immediately dismisses the overlay we just opened. Skip the // default restore whenever we're mid-handoff to a sibling overlay — mirrors // AgentPanel's identical guard (consumeAgentPanelOverlayFocusRestore). function consumeWorkspaceAppOverlayFocusRestore( pendingOverlayRef: { current: boolean }, event: { preventDefault: () => void }, ): void { if (!pendingOverlayRef.current) return; pendingOverlayRef.current = false; event.preventDefault(); } export function WorkspaceAppCard({ app, className, isPinned = false, onTogglePinned, }: { app: WorkspaceAppSummary; className?: string; isPinned?: boolean; onTogglePinned?: () => void; }) { const t = useT(); const href = workspaceAppHref(app); const directHref = app.status !== "pending" && !isWorkspaceSsoApp(app) && isPathMountedWorkspaceApp(app) ? workspaceAppDirectHref(app, "/") : null; const isPending = app.status === "pending"; const pendingLabel = app.statusLabel || "Builder branch"; const pendingOpenLabel = t("dispatch.pages.openBuilderBranch", { defaultValue: "Open in Builder", }); const isArchived = !!app.archived; const audience = app.audience ?? "internal"; const [editOpen, setEditOpen] = useState(false); const [keysOpen, setKeysOpen] = useState(false); const [resourcesOpen, setResourcesOpen] = useState(false); const [draftName, setDraftName] = useState(app.name); const [draftDescription, setDraftDescription] = useState( app.description || "", ); useEffect(() => { if (editOpen) return; setDraftName(app.name); setDraftDescription(app.description || ""); }, [app.description, app.name, editOpen]); const archive = useActionMutation("archive-workspace-app", { onError: (err) => toast.error(`Could not hide ${app.name}: ${stringifyError(err)}`), }); const unarchive = useActionMutation("unarchive-workspace-app", { onError: (err) => toast.error(`Could not restore ${app.name}: ${stringifyError(err)}`), }); const removePending = useActionMutation("remove-pending-workspace-app", { onError: (err) => toast.error(`Could not remove ${app.name}: ${stringifyError(err)}`), }); const updateMetadata = useActionMutation("update-workspace-app-metadata", { onSuccess: () => { toast.success(`Updated ${draftName.trim() || app.name}`); setEditOpen(false); }, onError: (err) => toast.error(`Could not update ${app.name}: ${stringifyError(err)}`), }); const handleArchive = () => { archive.mutate({ appId: app.id }); toast.success(`Hid ${app.name} from the Apps list`); }; const handleUnarchive = () => { unarchive.mutate({ appId: app.id }); toast.success(`Restored ${app.name} to the Apps list`); }; const handleRemovePending = () => { removePending.mutate({ appId: app.id }); toast.success(`Removed pending ${app.name}`); }; const handleMetadataSubmit = (event: FormEvent) => { event.preventDefault(); const name = draftName.trim(); if (!name) { toast.error("App name is required."); return; } updateMetadata.mutate({ appId: app.id, name, description: draftDescription.trim(), }); }; const pinLabel = t( isPinned ? "dispatch.pages.unpinApp" : "dispatch.pages.pinApp", { name: app.name, defaultValue: isPinned ? "Unpin this app" : "Pin this app", }, ); return ( <>

{app.name}

{app.description || isPending ? (

{app.description || pendingLabel}

) : null}
{isPending ? ( {pendingLabel} ) : null} {isArchived ? ( Hidden ) : null} {audience === "public" ? ( Public ) : null}
setEditOpen(true)} onKeys={() => setKeysOpen(true)} onResources={() => setResourcesOpen(true)} onArchive={handleArchive} onUnarchive={handleUnarchive} onRemovePending={handleRemovePending} labels={{ createdBy: t("agents.dashboardMetadataCreatedBy"), owner: t("dispatch.pages.appMetadataOwner"), teams: t("dispatch.pages.appMetadataTeams"), share: t("share.share", { defaultValue: "Share" }), }} />
Edit app details
setDraftName(event.target.value)} />