import type { WorkspaceCommandDefinition, WorkspaceRuntimeControlTarget, WorkspaceRuntimeService, } from "@paperclipai/shared"; import { listWorkspaceCommandDefinitions, matchWorkspaceRuntimeServiceToCommand, } from "@paperclipai/shared"; import { Activity, ExternalLink, Loader2, Play, RotateCcw, Square } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export type WorkspaceRuntimeAction = "start" | "stop" | "restart" | "run"; export type WorkspaceRuntimeControlRequest = WorkspaceRuntimeControlTarget & { action: WorkspaceRuntimeAction; }; export type WorkspaceRuntimeControlItem = { key: string; title: string; kind: "service" | "job"; statusLabel: string; lifecycle: "shared" | "ephemeral" | null; healthStatus: "unknown" | "healthy" | "unhealthy" | null; command: string | null; cwd: string | null; port: number | null; url: string | null; canStart: boolean; canRun: boolean; workspaceCommandId?: string | null; runtimeServiceId?: string | null; serviceIndex?: number | null; disabledReason?: string | null; }; export type WorkspaceRuntimeControlSections = { services: WorkspaceRuntimeControlItem[]; jobs: WorkspaceRuntimeControlItem[]; otherServices: WorkspaceRuntimeControlItem[]; }; type LegacyWorkspaceRuntimeControlItem = WorkspaceRuntimeControlItem & { status?: string | null; }; type WorkspaceRuntimeControlsProps = { sections: WorkspaceRuntimeControlSections; items?: never; isPending?: boolean; pendingRequest?: WorkspaceRuntimeControlRequest | null; serviceEmptyMessage?: string; jobEmptyMessage?: string; emptyMessage?: never; disabledHint?: string | null; onAction: (request: WorkspaceRuntimeControlRequest) => void; className?: string; square?: boolean; } | { sections?: never; items: LegacyWorkspaceRuntimeControlItem[]; isPending?: boolean; pendingRequest?: WorkspaceRuntimeControlRequest | null; serviceEmptyMessage?: never; jobEmptyMessage?: never; emptyMessage?: string; disabledHint?: string | null; onAction: (request: WorkspaceRuntimeControlRequest) => void; className?: string; square?: boolean; }; export function hasRunningRuntimeServices( runtimeServices: Array<{ status: string }> | null | undefined, ) { return (runtimeServices ?? []).some((service) => service.status === "starting" || service.status === "running"); } function buildServiceItem( command: WorkspaceCommandDefinition, runtimeService: WorkspaceRuntimeService | null, canStartServices: boolean, ): WorkspaceRuntimeControlItem { return { key: `command:${command.id}:${runtimeService?.id ?? "idle"}`, title: command.name, kind: "service", statusLabel: runtimeService?.status ?? "stopped", lifecycle: runtimeService?.lifecycle ?? command.lifecycle, healthStatus: runtimeService?.healthStatus ?? "unknown", command: runtimeService?.command ?? command.command, cwd: runtimeService?.cwd ?? command.cwd, port: runtimeService?.port ?? null, url: runtimeService?.url ?? null, canStart: canStartServices && !command.disabledReason, canRun: false, workspaceCommandId: command.id, runtimeServiceId: runtimeService?.id ?? null, serviceIndex: command.serviceIndex, disabledReason: command.disabledReason, }; } function buildJobItem( command: WorkspaceCommandDefinition, canRunJobs: boolean, ): WorkspaceRuntimeControlItem { return { key: `command:${command.id}`, title: command.name, kind: "job", statusLabel: "run once", lifecycle: null, healthStatus: null, command: command.command, cwd: command.cwd, port: null, url: null, canStart: false, canRun: canRunJobs && !command.disabledReason && Boolean(command.command), workspaceCommandId: command.id, runtimeServiceId: null, serviceIndex: null, disabledReason: command.disabledReason ?? (!command.command ? "This job is missing a command." : null), }; } export function buildWorkspaceRuntimeControlSections(input: { runtimeConfig: Record | null | undefined; runtimeServices: WorkspaceRuntimeService[] | null | undefined; canStartServices: boolean; canRunJobs?: boolean; }): WorkspaceRuntimeControlSections { const commands = listWorkspaceCommandDefinitions(input.runtimeConfig); const runtimeServices = [...(input.runtimeServices ?? [])]; const matchedRuntimeServiceIds = new Set(); const services: WorkspaceRuntimeControlItem[] = []; const jobs: WorkspaceRuntimeControlItem[] = []; for (const command of commands) { if (command.kind === "job") { jobs.push(buildJobItem(command, input.canRunJobs ?? input.canStartServices)); continue; } const runtimeService = matchWorkspaceRuntimeServiceToCommand(command, runtimeServices); if (runtimeService) matchedRuntimeServiceIds.add(runtimeService.id); services.push(buildServiceItem(command, runtimeService, input.canStartServices)); } const otherServices = runtimeServices .filter((runtimeService) => !matchedRuntimeServiceIds.has(runtimeService.id) && (runtimeService.status === "starting" || runtimeService.status === "running")) .map((runtimeService) => ({ key: `runtime:${runtimeService.id}`, title: runtimeService.serviceName, kind: "service" as const, statusLabel: runtimeService.status, lifecycle: runtimeService.lifecycle, healthStatus: runtimeService.healthStatus, command: runtimeService.command ?? null, cwd: runtimeService.cwd ?? null, port: runtimeService.port ?? null, url: runtimeService.url ?? null, canStart: false, canRun: false, workspaceCommandId: null, runtimeServiceId: runtimeService.id, serviceIndex: runtimeService.configIndex ?? null, disabledReason: "This runtime service no longer matches a configured workspace command.", })); return { services, jobs, otherServices, }; } export function buildWorkspaceRuntimeControlItems(input: { runtimeConfig: Record | null | undefined; runtimeServices: WorkspaceRuntimeService[] | null | undefined; canStartServices: boolean; canRunJobs?: boolean; }): LegacyWorkspaceRuntimeControlItem[] { return buildWorkspaceRuntimeControlSections(input).services.map((item) => ({ ...item, status: item.statusLabel, })); } function requestMatchesPending( pendingRequest: WorkspaceRuntimeControlRequest | null | undefined, nextRequest: WorkspaceRuntimeControlRequest, ) { return pendingRequest?.action === nextRequest.action && (pendingRequest?.workspaceCommandId ?? null) === (nextRequest.workspaceCommandId ?? null) && (pendingRequest?.runtimeServiceId ?? null) === (nextRequest.runtimeServiceId ?? null) && (pendingRequest?.serviceIndex ?? null) === (nextRequest.serviceIndex ?? null); } function buildRequest(item: WorkspaceRuntimeControlItem, action: WorkspaceRuntimeAction): WorkspaceRuntimeControlRequest { return { action, workspaceCommandId: item.workspaceCommandId ?? null, runtimeServiceId: item.runtimeServiceId ?? null, serviceIndex: item.serviceIndex ?? null, }; } function CommandActionButtons({ item, isPending, pendingRequest, onAction, square, }: { item: WorkspaceRuntimeControlItem; isPending: boolean; pendingRequest: WorkspaceRuntimeControlRequest | null | undefined; onAction: (request: WorkspaceRuntimeControlRequest) => void; square?: boolean; }) { const actions: WorkspaceRuntimeAction[] = item.kind === "job" ? ["run"] : item.statusLabel === "running" || item.statusLabel === "starting" ? ["stop", ...(item.canStart ? ["restart" as const] : [])] : ["start"]; return (
{actions.map((action) => { const request = buildRequest(item, action); const Icon = action === "stop" ? Square : action === "restart" ? RotateCcw : Play; const label = action === "run" ? "Run" : action === "start" ? "Start" : action === "stop" ? "Stop" : "Restart"; const showSpinner = isPending && requestMatchesPending(pendingRequest, request); const disabled = isPending || (action === "run" && !item.canRun) || ((action === "start" || action === "restart") && !item.canStart); return ( ); })}
); } function CommandSection({ title, description, items, emptyMessage, disabledHint, isPending, pendingRequest, onAction, square, }: { title: string; description: string; items: WorkspaceRuntimeControlItem[]; emptyMessage: string; disabledHint?: string | null; isPending: boolean; pendingRequest: WorkspaceRuntimeControlRequest | null | undefined; onAction: (request: WorkspaceRuntimeControlRequest) => void; square?: boolean; }) { return (
{title}

{description}

{items.length === 0 ? (
{emptyMessage} {disabledHint ?

{disabledHint}

: null}
) : (
{items.map((item) => (
{item.title}
{item.kind} · {item.statusLabel} {item.lifecycle ? ` · ${item.lifecycle}` : ""}
{item.url ? ( {item.url} ) : null} {item.port ?
Port {item.port}
: null} {item.command ?
{item.command}
: null} {item.cwd ?
{item.cwd}
: null} {item.disabledReason ?
{item.disabledReason}
: null}
{item.healthStatus && item.statusLabel !== "stopped" ? (
{item.healthStatus}
) : null}
))}
)}
); } export function WorkspaceRuntimeControls({ sections, items, isPending = false, pendingRequest = null, serviceEmptyMessage = "No services are configured for this workspace.", jobEmptyMessage = "No one-shot jobs are configured for this workspace.", emptyMessage, disabledHint = null, onAction, className, square, }: WorkspaceRuntimeControlsProps) { const resolvedSections = sections ?? { services: (items ?? []).map((item) => ({ ...item, statusLabel: item.statusLabel ?? item.status ?? "stopped", })), jobs: [], otherServices: [], }; const resolvedServiceEmptyMessage = emptyMessage ?? serviceEmptyMessage; const runningCount = [...resolvedSections.services, ...resolvedSections.otherServices].filter( (item) => item.statusLabel === "running" || item.statusLabel === "starting", ).length; const visibleDisabledHint = runningCount > 0 || disabledHint === null ? null : disabledHint; return (
Workspace commands
0 ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" : "border-border bg-background text-muted-foreground", )} > {runningCount > 0 ? `${runningCount} services running` : "No services running"} {resolvedSections.jobs.length > 0 ? `${resolvedSections.jobs.length} job${resolvedSections.jobs.length === 1 ? "" : "s"} available to run on demand.` : "Each command can be controlled independently."}
{visibleDisabledHint ?

{visibleDisabledHint}

: null}
{resolvedSections.otherServices.length > 0 ? ( ) : null}
); }