Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/openclaw-agent-integration.md
Original file line number Diff line number Diff line change
@@ -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.
155 changes: 45 additions & 110 deletions apps/webapp/app/components/AskAI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ import { useSearchParams } from "@remix-run/react";
import DOMPurify from "dompurify";
import { motion } from "framer-motion";
import { marked } from "marked";
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTypedRouteLoaderData } from "remix-typedjson";
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
import { SparkleListIcon } from "~/assets/icons/SparkleListIcon";
import { useFeatures } from "~/hooks/useFeatures";
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
import { type loader } from "~/root";
import { Button } from "./primitives/Buttons";
import { Callout } from "./primitives/Callout";
Expand All @@ -39,104 +38,6 @@ function useKapaWebsiteId() {
return routeMatch?.kapa.websiteId;
}

/** Open/close state for the Ask AI dialog, including the `?aiHelp=` deep-link handling. */
function useAskAIState() {
const [isOpen, setIsOpen] = useState(false);
const [initialQuery, setInitialQuery] = useState<string | undefined>();
const [searchParams, setSearchParams] = useSearchParams();

const openAskAI = useCallback((question?: string) => {
if (question) {
setInitialQuery(question);
} else {
setInitialQuery(undefined);
}
setIsOpen(true);
}, []);

const closeAskAI = useCallback(() => {
setIsOpen(false);
setInitialQuery(undefined);
}, []);

// Handle URL param functionality
useEffect(() => {
const aiHelp = searchParams.get("aiHelp");
if (aiHelp) {
// Delay to avoid hCaptcha bot detection
window.setTimeout(() => openAskAI(aiHelp), 1000);

// Clone instead of mutating in place
const next = new URLSearchParams(searchParams);
next.delete("aiHelp");
setSearchParams(next);
}
}, [searchParams, openAskAI]);

return { isOpen, setIsOpen, initialQuery, openAskAI, closeAskAI };
}

/**
* Hosts Ask AI (Kapa provider, ⌘I shortcut, dialog) for a menu that renders its own trigger. Wrap
* it around the popover, not inside, so the dialog and shortcut survive the popover closing.
* `children` receives the open function, or undefined when Ask AI is unavailable (self-hosted, no
* Kapa website id, or SSR).
*/
export function AskAIRoot({
children,
}: {
children: (openAskAI: (() => void) | undefined) => ReactNode;
}) {
const { isManagedCloud } = useFeatures();
const websiteId = useKapaWebsiteId();

if (!isManagedCloud || !websiteId) {
return <>{children(undefined)}</>;
}

return (
<ClientOnly fallback={<>{children(undefined)}</>}>
{() => <AskAIRootProvider websiteId={websiteId}>{children}</AskAIRootProvider>}
</ClientOnly>
);
}

function AskAIRootProvider({
websiteId,
children,
}: {
websiteId: string;
children: (openAskAI: () => void) => ReactNode;
}) {
const { isOpen, setIsOpen, initialQuery, openAskAI, closeAskAI } = useAskAIState();

useShortcutKeys({
shortcut: { modifiers: ["mod"], key: "i", enabledOnInputElements: true },
action: () => openAskAI(),
});

return (
<KapaProvider
integrationId={websiteId}
callbacks={{
askAI: {
onQuerySubmit: () => openAskAI(),
onAnswerGenerationCompleted: () => openAskAI(),
},
}}
botProtectionMechanism="hcaptcha"
>
{children(() => openAskAI())}
<AskAIDialog
initialQuery={initialQuery}
isOpen={isOpen}
onOpenChange={setIsOpen}
closeAskAI={closeAskAI}
/>
</KapaProvider>
);
}

export function AskAI({ isCollapsed = false }: { isCollapsed?: boolean }) {
const { isManagedCloud } = useFeatures();
const websiteId = useKapaWebsiteId();
Expand Down Expand Up @@ -171,7 +72,37 @@ type AskAIProviderProps = {
};

function AskAIProvider({ websiteId, isCollapsed = false }: AskAIProviderProps) {
const { isOpen, setIsOpen, initialQuery, openAskAI, closeAskAI } = useAskAIState();
const [isOpen, setIsOpen] = useState(false);
const [initialQuery, setInitialQuery] = useState<string | undefined>();
const [searchParams, setSearchParams] = useSearchParams();

const openAskAI = useCallback((question?: string) => {
if (question) {
setInitialQuery(question);
} else {
setInitialQuery(undefined);
}
setIsOpen(true);
}, []);

const closeAskAI = useCallback(() => {
setIsOpen(false);
setInitialQuery(undefined);
}, []);

// Handle URL param functionality
useEffect(() => {
const aiHelp = searchParams.get("aiHelp");
if (aiHelp) {
// Delay to avoid hCaptcha bot detection
window.setTimeout(() => openAskAI(aiHelp), 1000);

// Clone instead of mutating in place
const next = new URLSearchParams(searchParams);
next.delete("aiHelp");
setSearchParams(next);
}
}, [searchParams, openAskAI]);

return (
<KapaProvider
Expand Down Expand Up @@ -203,7 +134,11 @@ function AskAIProvider({ websiteId, isCollapsed = false }: AskAIProviderProps) {
</Button>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8} className="flex items-center gap-2 text-xs">
<TooltipContent
side="right"
sideOffset={8}
className="flex items-center gap-2 text-xs"
>
Ask AI
<span className="flex items-center">
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
Expand Down Expand Up @@ -242,7 +177,7 @@ function AskAIDialog({ initialQuery, isOpen, onOpenChange, closeAskAI }: AskAIDi
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent className="animated-gradient-glow flex max-h-[90vh] min-h-fit w-full flex-col justify-between gap-0 px-0 pb-0 pt-0 sm:max-w-prose">
<DialogHeader className="flex h-11 items-start justify-center rounded-t-md bg-background-bright pl-3">
<DialogHeader className="flex h-[2.75rem] items-start justify-center rounded-t-md bg-background-bright pl-3">
<div className="flex items-center gap-1">
<AISparkleIcon className="size-5" />
<DialogTitle className="text-sm font-medium text-text-bright">Ask AI</DialogTitle>
Expand Down Expand Up @@ -295,7 +230,7 @@ function ChatMessages({
];

return (
<div className="flex-1 overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
<div className="flex-1 overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-300">
{conversation.length === 0 ? (
<motion.div
className="flex flex-col gap-2 pb-2"
Expand All @@ -318,7 +253,7 @@ function ChatMessages({
{exampleQuestions.map((question, index) => (
<motion.button
key={index}
className="group flex w-fit items-center gap-2 rounded-full border border-dashed border-border-bright px-4 py-2 transition-colors hover:border-solid hover:border-indigo-500"
className="group flex w-fit items-center gap-2 rounded-full border border-dashed border-charcoal-600 px-4 py-2 transition-colors hover:border-solid hover:border-indigo-500"
onClick={() => onExampleClick(question)}
variants={{
hidden: {
Expand Down Expand Up @@ -537,7 +472,7 @@ function ChatInterface({ initialQuery }: { initialQuery?: string }) {
error={error}
addFeedback={addFeedback}
/>
<form onSubmit={handleSubmit} className="shrink-0 border-t border-grid-bright p-4">
<form onSubmit={handleSubmit} className="flex-shrink-0 border-t border-grid-bright p-4">
<div className="flex gap-3">
<input
type="text"
Expand Down Expand Up @@ -580,7 +515,7 @@ function ChatInterface({ initialQuery }: { initialQuery?: string }) {
disabled={!message.trim()}
LeadingIcon={<ArrowUpIcon className="size-5 text-text-bright" />}
variant="primary/large"
className="size-10 min-w-10 rounded-full group-disabled/button:border-border-brighter group-disabled/button:bg-surface-control"
className="size-10 min-w-10 rounded-full group-disabled/button:border-charcoal-550 group-disabled/button:bg-charcoal-600"
/>
)}
</div>
Expand All @@ -600,11 +535,11 @@ function GradientSpinnerBackground({
}) {
return (
<div
className={`flex rounded-full bg-linear-to-br from-indigo-500 via-purple-500 to-fuchsia-500 p-px ${className}`}
className={`flex rounded-full bg-gradient-to-br from-indigo-500 via-purple-500 to-fuchsia-500 p-px ${className}`}
>
<div
className={`flex h-full w-full items-center justify-center rounded-full bg-surface-control ${
hoverEffect ? "transition group-hover:bg-surface-control-hover" : ""
className={`flex h-full w-full items-center justify-center rounded-full bg-charcoal-600 ${
hoverEffect ? "transition group-hover:bg-charcoal-550" : ""
}`}
>
{children}
Expand Down
6 changes: 3 additions & 3 deletions apps/webapp/app/components/ErrorDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ type DisplayOptionsProps = {

export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) {
return (
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center overflow-y-auto bg-[#16181C]">
<div className="z-10 mt-[30vh] flex shrink-0 flex-col items-center gap-8">
<div className="relative flex min-h-screen flex-col items-center justify-center bg-background-dimmed">
<div className="z-10 mt-[30vh] flex flex-col items-center gap-8">
<Header1>{title}</Header1>
{message && <Paragraph>{message}</Paragraph>}
<LinkButton
to={button ? button.to : "/"}
shortcut={{ key: "enter" }}
shortcut={{ modifiers: ["mod"], key: "g" }}
variant="primary/medium"
LeadingIcon={HomeIcon}
>
Expand Down
28 changes: 8 additions & 20 deletions apps/webapp/app/components/LogoIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
export function LogoIcon({ className }: { className?: string }) {
return (
<svg
width="321"
height="282"
viewBox="0 0 321 282"
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<rect width="32" height="32" rx="8" fill="#2E7D32" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M96.1017 113.4L160.679 4.57764e-05L320.718 281.045H0.638916L65.2159 167.642L110.896 194.382L92.0035 227.561H229.354L160.679 106.965L141.786 140.144L96.1017 113.4Z"
fill="url(#paint0_linear_465_1663)"
d="M16 6L24 24H8L16 6Z"
fill="white"
fillOpacity="0.9"
/>
<defs>
<linearGradient
id="paint0_linear_465_1663"
x1="320.718"
y1="140.687"
x2="0.638918"
y2="140.687"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41FF54" />
<stop offset="1" stopColor="#E7FF52" />
</linearGradient>
</defs>
<circle cx="16" cy="20" r="2" fill="#2E7D32" />
Comment on lines +4 to +17

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 LogoIcon and LogoType completely replaced with different brand identity

The Trigger.dev logo SVGs in LogoIcon.tsx and LogoType.tsx have been entirely replaced. The original logo was a detailed geometric shape with a green-to-yellow gradient (#41FF54 to #E7FF52), consistent with Trigger.dev's established brand. The new logo is a green rounded rectangle (#2E7D32) with a simple white triangle and dot — a completely different design language. The LogoType also replaces the detailed path-based wordmark with an SVG <text> element rendering "Trigger.dev", which may render differently across browsers depending on font availability (Geist Variable, Inter, Helvetica Neue, sans-serif). This is a significant brand change that should be verified as intentional.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

</svg>
);
}
Loading