From ac862c69b083389a702bb968b2e0134f7a0bf866 Mon Sep 17 00:00:00 2001 From: deepshekhardas Date: Fri, 19 Jun 2026 10:59:25 +0530 Subject: [PATCH] feat: OpenClaw agent integration with Slack webhooks Squashed rebase of pr-3266-fix onto official/main. Co-authored-by: James Ritchie --- .changeset/openclaw-agent-integration.md | 8 + apps/webapp/app/components/AskAI.tsx | 2 +- apps/webapp/app/components/ErrorDisplay.tsx | 2 +- apps/webapp/app/components/LogoIcon.tsx | 28 +- apps/webapp/app/components/LogoType.tsx | 206 ++------------- .../app/components/code/AIQueryInput.tsx | 4 +- .../webapp/app/components/code/TSQLEditor.tsx | 2 +- .../app/components/code/TSQLResultsTable.tsx | 4 +- .../app/components/logs/LogDetailView.tsx | 2 +- apps/webapp/app/components/logs/LogsTable.tsx | 2 +- .../navigation/EnvironmentSelector.tsx | 4 +- .../OrganizationSettingsSideMenu.tsx | 2 +- .../app/components/navigation/SideMenu.tsx | 6 +- .../components/navigation/SideMenuHeader.tsx | 2 +- .../onboarding/TechnologyPicker.tsx | 2 +- .../app/components/primitives/Select.tsx | 2 +- .../app/components/primitives/Sheet.tsx | 2 +- .../primitives/TreeView/TreeView.tsx | 2 +- .../primitives/charts/ChartLegendCompound.tsx | 2 +- .../primitives/charts/ChartZoom.tsx | 4 +- .../components/runs/v3/ReplayRunDialog.tsx | 4 +- .../app/components/scheduled/timezones.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 6 +- .../route.tsx | 2 +- .../QueryHelpSidebar.tsx | 10 +- .../QueryHistoryPopover.tsx | 2 +- .../route.tsx | 4 +- .../route.tsx | 2 +- .../route.tsx | 2 +- .../app/routes/_app.timezones/route.tsx | 2 +- .../app/routes/agents.$agentId.status.tsx | 189 ++++++++++++++ apps/webapp/app/routes/agents.setup.tsx | 246 ++++++++++++++++++ .../webapp/app/routes/api.agents.provision.ts | 92 +++++++ .../route.tsx | 2 +- ...ectParam.env.$envParam.runs.bulkaction.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 4 +- ...ctParam.schedules.new.natural-language.tsx | 2 +- .../app/routes/storybook.popover/route.tsx | 2 +- apps/webapp/app/routes/storybook/route.tsx | 2 +- apps/webapp/app/routes/webhooks.slack.ts | 140 ++++++++++ apps/webapp/remix.config.js | 2 +- .../migration.sql | 121 +++++++++ .../database/prisma/schema.prisma | 102 ++++++++ .../emails/emails/components/Footer.tsx | 2 +- 47 files changed, 983 insertions(+), 255 deletions(-) create mode 100644 .changeset/openclaw-agent-integration.md create mode 100644 apps/webapp/app/routes/agents.$agentId.status.tsx create mode 100644 apps/webapp/app/routes/agents.setup.tsx create mode 100644 apps/webapp/app/routes/api.agents.provision.ts create mode 100644 apps/webapp/app/routes/webhooks.slack.ts create mode 100644 internal-packages/database/prisma/migrations/20260325122458_add_openclaw_agents/migration.sql diff --git a/.changeset/openclaw-agent-integration.md b/.changeset/openclaw-agent-integration.md new file mode 100644 index 00000000000..6070c27fb7c --- /dev/null +++ b/.changeset/openclaw-agent-integration.md @@ -0,0 +1,8 @@ +--- +"@trigger.dev/core": patch +"@trigger.dev/sdk": patch +--- + +feat: Add OpenClaw agent integration with Slack webhooks + +Implements Phase 1 MVP for AI agent platform allowing users to create agents through setup form (/agents/setup). Agents are stored in database with configuration (model, platform, tools). Slack webhook receives messages and triggers agent responses. Includes agent management UI and webhook integration infrastructure. diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx index 814d4649c8f..4b20e99f760 100644 --- a/apps/webapp/app/components/AskAI.tsx +++ b/apps/webapp/app/components/AskAI.tsx @@ -230,7 +230,7 @@ function ChatMessages({ ]; return ( -
+
{conversation.length === 0 ? ( +
{title} {message && {message}} diff --git a/apps/webapp/app/components/LogoIcon.tsx b/apps/webapp/app/components/LogoIcon.tsx index 365c7c90c63..0da161c4b08 100644 --- a/apps/webapp/app/components/LogoIcon.tsx +++ b/apps/webapp/app/components/LogoIcon.tsx @@ -1,32 +1,20 @@ export function LogoIcon({ className }: { className?: string }) { return ( + - - - - - - + ); } diff --git a/apps/webapp/app/components/LogoType.tsx b/apps/webapp/app/components/LogoType.tsx index 76a88fce1a1..75d4942ef5d 100644 --- a/apps/webapp/app/components/LogoType.tsx +++ b/apps/webapp/app/components/LogoType.tsx @@ -1,190 +1,32 @@ export function LogoType({ className }: { className?: string }) { return ( - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + {/* Icon */} + + + + {/* Text */} + + Trigger.dev + ); } diff --git a/apps/webapp/app/components/code/AIQueryInput.tsx b/apps/webapp/app/components/code/AIQueryInput.tsx index cd5e9db3bd8..c81362e7e30 100644 --- a/apps/webapp/app/components/code/AIQueryInput.tsx +++ b/apps/webapp/app/components/code/AIQueryInput.tsx @@ -257,7 +257,7 @@ export function AIQueryInput({ onChange={(e) => setPrompt(e.target.value)} disabled={isLoading} rows={8} - className="m-0 min-h-10 w-full resize-none border-0 bg-background-bright px-3 py-2.5 text-sm text-text-bright scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600 file:border-0 file:bg-transparent file:text-base file:font-medium placeholder:text-text-dimmed focus:border-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50" + className="m-0 min-h-10 w-full resize-none border-0 bg-background-bright px-3 py-2.5 text-sm text-text-bright scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-300 file:border-0 file:bg-transparent file:text-base file:font-medium placeholder:text-text-dimmed focus:border-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50" onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey && prompt.trim() && !isLoading) { e.preventDefault(); @@ -390,7 +390,7 @@ export function AIQueryInput({ )}
-
+
{thinking}

}> {thinking}
diff --git a/apps/webapp/app/components/code/TSQLEditor.tsx b/apps/webapp/app/components/code/TSQLEditor.tsx index 1fa56a2cea8..9ab93569d0e 100644 --- a/apps/webapp/app/components/code/TSQLEditor.tsx +++ b/apps/webapp/app/components/code/TSQLEditor.tsx @@ -271,7 +271,7 @@ export function TSQLEditor(opts: TSQLEditorProps) { >
{ diff --git a/apps/webapp/app/components/code/TSQLResultsTable.tsx b/apps/webapp/app/components/code/TSQLResultsTable.tsx index 73ca07180bf..2f4f96da759 100644 --- a/apps/webapp/app/components/code/TSQLResultsTable.tsx +++ b/apps/webapp/app/components/code/TSQLResultsTable.tsx @@ -1113,7 +1113,7 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({ return (
@@ -1168,7 +1168,7 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({ return (
diff --git a/apps/webapp/app/components/logs/LogDetailView.tsx b/apps/webapp/app/components/logs/LogDetailView.tsx index a44d833054f..152862a2d89 100644 --- a/apps/webapp/app/components/logs/LogDetailView.tsx +++ b/apps/webapp/app/components/logs/LogDetailView.tsx @@ -146,7 +146,7 @@ export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDet className="pl-1" /> -
+
diff --git a/apps/webapp/app/components/logs/LogsTable.tsx b/apps/webapp/app/components/logs/LogsTable.tsx index ed8e6793e5f..9cfd5043d93 100644 --- a/apps/webapp/app/components/logs/LogsTable.tsx +++ b/apps/webapp/app/components/logs/LogsTable.tsx @@ -115,7 +115,7 @@ export function LogsTable({ }, [hasMore, isLoadingMore, onLoadMore]); return ( -
+
diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index 487b3ec0496..3a0d05b9b68 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -96,7 +96,7 @@ export function EnvironmentSelector({ disableHoverableContent /> Back to app -
+
diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index eef6a14c736..90544998742 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -335,7 +335,7 @@ export function SideMenu({ "min-h-0 overflow-y-auto pt-2", isCollapsed ? "scrollbar-none" - : "scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" + : "scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-300" )} ref={borderRef} > @@ -883,7 +883,7 @@ function ProjectSelector({ disableHoverableContent />
{children}
diff --git a/apps/webapp/app/components/onboarding/TechnologyPicker.tsx b/apps/webapp/app/components/onboarding/TechnologyPicker.tsx index 9c9f9a6b24a..169056573f4 100644 --- a/apps/webapp/app/components/onboarding/TechnologyPicker.tsx +++ b/apps/webapp/app/components/onboarding/TechnologyPicker.tsx @@ -308,7 +308,7 @@ export function TechnologyPicker({ />
- + {filteredOptions.map((option) => ( diff --git a/apps/webapp/app/components/primitives/Sheet.tsx b/apps/webapp/app/components/primitives/Sheet.tsx index 49ad15fe0ee..3c77229aa92 100644 --- a/apps/webapp/app/components/primitives/Sheet.tsx +++ b/apps/webapp/app/components/primitives/Sheet.tsx @@ -171,7 +171,7 @@ SheetContent.displayName = SheetPrimitive.Content.displayName; export const SheetBody = ({ className, ...props }: React.HTMLAttributes) => (
({ } }} className={cn( - "w-full overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600 focus-within:outline-none", + "w-full overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-300 focus-within:outline-none", parentClassName )} layoutScroll diff --git a/apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx b/apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx index 7fe77d97e81..daaa99e20a2 100644 --- a/apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx @@ -201,7 +201,7 @@ export function ChartLegendCompound({ className={cn( "flex flex-col", scrollable && - "min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" + "min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-300" )} > {legendItems.visible.map((item) => { diff --git a/apps/webapp/app/components/primitives/charts/ChartZoom.tsx b/apps/webapp/app/components/primitives/charts/ChartZoom.tsx index 4b2e921630b..eedf4e43f2f 100644 --- a/apps/webapp/app/components/primitives/charts/ChartZoom.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartZoom.tsx @@ -127,7 +127,7 @@ export function ZoomTooltip({ "absolute whitespace-nowrap rounded border px-2 py-1 text-xxs tabular-nums", invalidSelection ? "border-amber-800 bg-amber-950 text-amber-400" - : "border-blue-800 bg-[#1B2334] text-blue-400" + : "border-blue-800 bg-blue-50 text-blue-400" )} style={{ left: coordinate?.x, @@ -141,7 +141,7 @@ export function ZoomTooltip({ "absolute -top-[5px] left-1/2 h-2 w-2 -translate-x-1/2 rotate-45", invalidSelection ? "border-l border-t border-amber-800 bg-amber-950" - : "border-l border-t border-blue-800 bg-[#1B2334]" + : "border-l border-t border-blue-800 bg-blue-50" )} />
diff --git a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx index e42a2122abe..e729675cedd 100644 --- a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx +++ b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx @@ -239,7 +239,7 @@ function ReplayForm({ className="-mx-3 mt-3 w-auto flex-1 border-b border-t border-grid-dimmed" > -
+
-
+
Options enable you to control the execution behavior of your task.{" "} diff --git a/apps/webapp/app/components/scheduled/timezones.tsx b/apps/webapp/app/components/scheduled/timezones.tsx index 779afd1dd51..9c11eb0a678 100644 --- a/apps/webapp/app/components/scheduled/timezones.tsx +++ b/apps/webapp/app/components/scheduled/timezones.tsx @@ -14,7 +14,7 @@ export function TimezoneList({ timezones }: { timezones: string[] }) { return (
{/* Scrollable content */} -
+
{/* Progress meter for v2 batches */} {showProgressMeter && ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx index f05ec143817..b1cf007c0cc 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx @@ -195,7 +195,7 @@ export default function Page() { ) : null}
-
+
-
+
@@ -530,7 +530,7 @@ export default function Page() { {deployment.errorData && } {deployment.tasks && ( -
+
@@ -692,7 +692,7 @@ function LogsDisplay({
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx index 06847acd1de..7cfa8fcc26e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx @@ -295,7 +295,7 @@ export default function Page() { New environment variables
-
+
{selectedBranchId ? ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHelpSidebar.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHelpSidebar.tsx index daa7187acf2..7311edba3a9 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHelpSidebar.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHelpSidebar.tsx @@ -36,7 +36,7 @@ export function QueryHelpSidebar({ onValueChange={onTabChange} className="flex min-h-0 flex-col overflow-hidden pt-1" > -
+
@@ -97,7 +97,7 @@ export function QueryHelpSidebar({
@@ -105,7 +105,7 @@ export function QueryHelpSidebar({
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHistoryPopover.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHistoryPopover.tsx index de7e7a79e03..76b66643f5d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHistoryPopover.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHistoryPopover.tsx @@ -111,7 +111,7 @@ export function QueryHistoryPopover({ sideOffset={6} style={{ maxHeight: "var(--radix-popover-content-available-height)" }} > -
+
{history.map((item) => { // Format time filter display diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index de23b935cd6..d46ad9b6d53 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -1137,7 +1137,7 @@ function TasksTreeView({ Shortcuts Keyboard shortcuts @@ -1233,7 +1233,7 @@ function TimelineView({ return (
-
+
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx index 0eb496880a4..a324899cff2 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx @@ -589,7 +589,7 @@ function HexPopover({ avatar, hex }: { avatar: Avatar; hex: string }) { diff --git a/apps/webapp/app/routes/_app.timezones/route.tsx b/apps/webapp/app/routes/_app.timezones/route.tsx index b9e2704ff6e..407200d2c3c 100644 --- a/apps/webapp/app/routes/_app.timezones/route.tsx +++ b/apps/webapp/app/routes/_app.timezones/route.tsx @@ -21,7 +21,7 @@ export default function Page() {
-
+
Supported timezones We support these timezones when creating a schedule.
    diff --git a/apps/webapp/app/routes/agents.$agentId.status.tsx b/apps/webapp/app/routes/agents.$agentId.status.tsx new file mode 100644 index 00000000000..e4a821a04c5 --- /dev/null +++ b/apps/webapp/app/routes/agents.$agentId.status.tsx @@ -0,0 +1,189 @@ +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { Header1, Header2 } from "~/components/primitives/Headers"; +import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { prisma } from "~/db.server"; +import { requireUser } from "~/services/auth.server"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableHeaderCell, + TableRow, +} from "~/components/primitives/Table"; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const user = await requireUser(request); + const { agentId } = params; + + if (!agentId) { + throw new Response("Not found", { status: 404 }); + } + + // Get agent config + const agentConfig = await prisma.agentConfig.findUnique({ + where: { id: agentId }, + include: { + executions: { + orderBy: { createdAt: "desc" }, + take: 10, + }, + healthChecks: { + orderBy: { createdAt: "desc" }, + take: 5, + }, + }, + }); + + if (!agentConfig || agentConfig.userId !== user.id) { + throw new Response("Not found", { status: 404 }); + } + + return typedjson({ + agentConfig, + }); +}; + +function getStatusColor(status: string) { + switch (status) { + case "healthy": + return "text-green-600 bg-green-50"; + case "unhealthy": + return "text-red-600 bg-red-50"; + case "provisioning": + return "text-yellow-600 bg-yellow-50"; + default: + return "text-gray-600 bg-gray-50"; + } +} + +export default function AgentStatus() { + const { agentConfig } = useTypedLoaderData(); + + return ( + + + {agentConfig.name} + +
    + {/* Basic Info */} +
    + Configuration +
    +
    + Status: + + {agentConfig.status} + +
    +
    + Model: + {agentConfig.model} +
    +
    + Platform: + {agentConfig.messagingPlatform} +
    +
    + Container: + + {agentConfig.containerName && agentConfig.containerPort + ? `${agentConfig.containerName}:${agentConfig.containerPort}` + : "Not provisioned"} + +
    +
    + Created: + {new Date(agentConfig.createdAt).toLocaleString()} +
    +
    +
    + + {/* Tools */} +
    + Tools +
    + {Array.isArray(agentConfig.tools) && agentConfig.tools.length > 0 ? ( +
      + {(agentConfig.tools as string[]).map((tool) => ( +
    • + ✓ {tool} +
    • + ))} +
    + ) : ( + No tools configured + )} +
    +
    +
    + + {/* Recent Executions */} + {agentConfig.executions.length > 0 && ( +
    + Recent Executions +
+ + + Message + Response + Time (ms) + Date + + + + {agentConfig.executions.map((exec) => ( + + {exec.message} + {exec.response} + {exec.executionTimeMs}ms + {new Date(exec.createdAt).toLocaleString()} + + ))} + +
+
+ )} + + {/* Health History */} + {agentConfig.healthChecks.length > 0 && ( +
+ Health Checks + + + + Status + Response Time + Date + + + + {agentConfig.healthChecks.map((check) => ( + + + + {check.isHealthy ? "✓ Healthy" : "✗ Unhealthy"} + + + {check.responseTimeMs}ms + {new Date(check.createdAt).toLocaleString()} + + ))} + +
+
+ )} + + + ); +} diff --git a/apps/webapp/app/routes/agents.setup.tsx b/apps/webapp/app/routes/agents.setup.tsx new file mode 100644 index 00000000000..efc6d70171c --- /dev/null +++ b/apps/webapp/app/routes/agents.setup.tsx @@ -0,0 +1,246 @@ +import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; +import { json, redirect } from "@remix-run/node"; +import { Form, useActionData, useNavigation } from "@remix-run/react"; +import { useState } from "react"; +import { z } from "zod"; +import { Button } from "~/components/primitives/Buttons"; +import { Header1, Header2 } from "~/components/primitives/Headers"; +import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { prisma } from "~/db.server"; +import { requireUser } from "~/services/auth.server"; +import { logger } from "~/services/logger.server"; + +const SetupSchema = z.object({ + agentName: z.string().min(1, "Agent name is required"), + model: z.enum(["claude-3.5-sonnet", "claude-3-opus", "gpt-4-turbo"]), + messagingPlatform: z.enum(["slack", "discord", "telegram"]), + tools: z.string(), // JSON string array + slackWorkspaceId: z.string().optional(), + slackWebhookToken: z.string().optional(), +}); + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const user = await requireUser(request); + return json({ user }); +}; + +export const action = async ({ request }: ActionFunctionArgs) => { + if (request.method !== "POST") { + return json({ error: "Method not allowed" }, { status: 405 }); + } + + const user = await requireUser(request); + const formData = await request.formData(); + + try { + const data = SetupSchema.parse({ + agentName: formData.get("agentName"), + model: formData.get("model"), + messagingPlatform: formData.get("messagingPlatform"), + tools: formData.get("tools"), + slackWorkspaceId: formData.get("slackWorkspaceId"), + slackWebhookToken: formData.get("slackWebhookToken"), + }); + + // Parse tools JSON + const tools = JSON.parse(data.tools || "[]"); + + // Create agent config in database + const agentConfig = await prisma.agentConfig.create({ + data: { + name: data.agentName, + model: data.model, + messagingPlatform: data.messagingPlatform, + tools: tools, + slackWorkspaceId: data.slackWorkspaceId || null, + slackWebhookToken: data.slackWebhookToken || null, + userId: user.id, + status: "provisioning", + }, + }); + + logger.info("Agent created", { + agentId: agentConfig.id, + userId: user.id, + name: data.agentName, + }); + + // Trigger provisioning endpoint to spin up container + try { + const provisionResponse = await fetch("http://localhost:3000/api/agents/provision", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ agentId: agentConfig.id }), + }); + + if (!provisionResponse.ok) { + logger.error("Provisioning failed", { + agentId: agentConfig.id, + status: provisionResponse.status, + }); + } + } catch (error) { + logger.error("Failed to call provisioning endpoint", { error }); + } + + return redirect(`/agents/${agentConfig.id}/status`); + } catch (error) { + logger.error("Failed to create agent", { error, userId: user.id }); + return json( + { error: error instanceof Error ? error.message : "Failed to create agent" }, + { status: 400 } + ); + } +}; + +export default function AgentSetup() { + const navigation = useNavigation(); + const actionData = useActionData(); + const [selectedTools, setSelectedTools] = useState([]); + + const toolOptions = [ + { id: "web-search", label: "Web Search" }, + { id: "code-execution", label: "Code Execution" }, + { id: "file-operations", label: "File Operations" }, + { id: "api-calls", label: "API Calls" }, + ]; + + const handleToolChange = (toolId: string, checked: boolean) => { + if (checked) { + setSelectedTools([...selectedTools, toolId]); + } else { + setSelectedTools(selectedTools.filter((t) => t !== toolId)); + } + }; + + return ( + + + Create a New Agent + Set up your AI agent with model, messaging, and tools + + + {actionData?.error && ( +
+ {actionData.error} +
+ )} + + {/* Agent Name */} +
+ + +
+ + {/* Model Selection */} +
+ + +
+ + {/* Messaging Platform */} +
+ + +
+ + {/* Tools Selection */} +
+ Select Tools +
+ {toolOptions.map((tool) => ( + + ))} +
+ +
+ + {/* Slack Integration (conditional) */} + {/* This would be conditional based on messagingPlatform selection */} +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+ ); +} diff --git a/apps/webapp/app/routes/api.agents.provision.ts b/apps/webapp/app/routes/api.agents.provision.ts new file mode 100644 index 00000000000..777fda0ca5a --- /dev/null +++ b/apps/webapp/app/routes/api.agents.provision.ts @@ -0,0 +1,92 @@ +import type { ActionFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { spawn } from "child_process"; +import { prisma } from "~/db.server"; +import { logger } from "~/services/logger.server"; + +/** + * POST /api/agents/provision + * Provisions an OpenClaw container for a given agent config + */ +export const action = async ({ request }: ActionFunctionArgs) => { + if (request.method !== "POST") { + return json({ error: "Method not allowed" }, { status: 405 }); + } + + const { agentId } = await request.json() as { agentId: string }; + + if (!agentId) { + return json({ error: "agentId is required" }, { status: 400 }); + } + + try { + // Get agent config + const agentConfig = await prisma.agentConfig.findUnique({ + where: { id: agentId }, + }); + + if (!agentConfig) { + return json({ error: "Agent not found" }, { status: 404 }); + } + + // Find the next available port (starting at 8001) + const lastAgent = await prisma.agentConfig.findFirst({ + where: { + containerPort: { not: null }, + }, + orderBy: { containerPort: "desc" }, + }); + + const nextPort = (lastAgent?.containerPort || 8000) + 1; + + // Generate container name + const containerName = `openclaw-${agentConfig.userId.slice(0, 8)}-${agentConfig.id.slice(0, 8)}`; + + logger.info("Provisioning OpenClaw container", { + agentId, + containerName, + port: nextPort, + }); + + // TODO: Implement actual Docker provisioning + // For now, just update the database with port info + // Production would SSH to VPS and run: docker run -d --name $containerName -p $nextPort:8000 openclaw:latest + + const updatedAgent = await prisma.agentConfig.update({ + where: { id: agentId }, + data: { + containerName, + containerPort: nextPort, + status: "provisioning", + }, + }); + + // Log provisioning start (not health check yet since container doesn't exist) + await prisma.agentHealthCheck.create({ + data: { + agentId, + isHealthy: false, + errorMessage: "Container provisioning started - awaiting actual deployment", + }, + }); + + logger.info("Agent provisioned successfully", { + agentId, + containerName, + port: nextPort, + }); + + return json({ + success: true, + agentId, + containerName, + containerPort: nextPort, + }); + } catch (error) { + logger.error("Failed to provision agent", { error, agentId }); + return json( + { error: error instanceof Error ? error.message : "Provisioning failed" }, + { status: 500 } + ); + } +}; diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route.tsx index 60233d6d38f..75c5b30b02f 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route.tsx @@ -387,7 +387,7 @@ export function RealtimeStreamViewer({ {/* Content */}
{error && (
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx index 483e0ea5725..3211dbb0518 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx @@ -259,7 +259,7 @@ export function CreateBulkActionInspector({ className="pl-1" />
-
+
-
+
{submitFetcher ? : null} {schedule && } diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx index 670aee165c1..d0180c1a636 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx @@ -359,8 +359,8 @@ function CompleteManualWaitpointForm({ waitpoint }: { waitpoint: { id: string } contentClassName="normal-case tracking-normal max-w-xs" />
-
-
+
+
setText(e.target.value)} rows={3} - className="m-0 min-h-10 w-full border-0 bg-background-bright px-3 py-2 text-sm text-text-bright scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600 file:border-0 file:bg-transparent file:text-base file:font-medium focus:border-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50" + className="m-0 min-h-10 w-full border-0 bg-background-bright px-3 py-2 text-sm text-text-bright scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-300 file:border-0 file:bg-transparent file:text-base file:font-medium focus:border-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50" />