| State |
- Role |
+ For |
Invited by |
Created |
Join request |
@@ -312,7 +312,7 @@ export function CompanyInvites() {
{formatInviteState(invite.state)}
- {invite.humanRole ?? "—"} |
+ {formatInviteAudience(invite)} |
{invite.invitedByUser?.name || invite.invitedByUser?.email || "Unknown inviter"}
{invite.invitedByUser?.email && invite.invitedByUser.name ? (
@@ -372,3 +372,9 @@ export function CompanyInvites() {
function formatInviteState(state: "active" | "accepted" | "expired" | "revoked") {
return state.charAt(0).toUpperCase() + state.slice(1);
}
+
+function formatInviteAudience(invite: Awaited>["invites"][number]) {
+ if (invite.allowedJoinTypes === "agent") return "Agent";
+ if (invite.allowedJoinTypes === "both") return invite.humanRole ? `Human or agent · ${invite.humanRole}` : "Human or agent";
+ return invite.humanRole ?? "Human";
+}
diff --git a/ui/src/pages/CompanySettings.tsx b/ui/src/pages/CompanySettings.tsx
index 41af7db3..5fce4d05 100644
--- a/ui/src/pages/CompanySettings.tsx
+++ b/ui/src/pages/CompanySettings.tsx
@@ -7,25 +7,17 @@ import {
import { useCompany } from "../context/CompanyContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { companiesApi } from "../api/companies";
-import { accessApi } from "../api/access";
import { assetsApi } from "../api/assets";
import { instanceSettingsApi } from "../api/instanceSettings";
import { queryKeys } from "../lib/queryKeys";
import { Button } from "@/components/ui/button";
-import { Settings, Check, CloudUpload, Download, Upload } from "lucide-react";
+import { Settings, CloudUpload, Download, Upload } from "lucide-react";
import { CompanyPatternIcon } from "../components/CompanyPatternIcon";
import {
Field,
ToggleField,
- HintIcon,
} from "../components/agent-config-primitives";
-type AgentSnippetInput = {
- onboardingTextUrl: string;
- connectionCandidates?: string[] | null;
- testResolutionUrl?: string | null;
-};
-
const BYTES_PER_MIB = 1024 * 1024;
const DEFAULT_COMPANY_ATTACHMENT_MAX_MIB = DEFAULT_COMPANY_ATTACHMENT_MAX_BYTES / BYTES_PER_MIB;
const MAX_COMPANY_ATTACHMENT_MAX_MIB = MAX_COMPANY_ATTACHMENT_MAX_BYTES / BYTES_PER_MIB;
@@ -60,11 +52,6 @@ export function CompanySettings() {
setLogoUrl(selectedCompany.logoUrl ?? "");
}, [selectedCompany]);
- const [inviteError, setInviteError] = useState(null);
- const [inviteSnippet, setInviteSnippet] = useState(null);
- const [snippetCopied, setSnippetCopied] = useState(false);
- const [snippetCopyDelightId, setSnippetCopyDelightId] = useState(0);
-
const attachmentMaxBytes = Number.parseInt(attachmentMaxMiB, 10) * BYTES_PER_MIB;
const attachmentMaxValid =
Number.isInteger(attachmentMaxBytes)
@@ -101,59 +88,6 @@ export function CompanySettings() {
}
});
- const inviteMutation = useMutation({
- mutationFn: () =>
- accessApi.createOpenClawInvitePrompt(selectedCompanyId!),
- onSuccess: async (invite) => {
- setInviteError(null);
- const base = window.location.origin.replace(/\/+$/, "");
- const onboardingTextLink =
- invite.onboardingTextUrl ??
- invite.onboardingTextPath ??
- `/api/invites/${invite.token}/onboarding.txt`;
- const absoluteUrl = onboardingTextLink.startsWith("http")
- ? onboardingTextLink
- : `${base}${onboardingTextLink}`;
- setSnippetCopied(false);
- setSnippetCopyDelightId(0);
- let snippet: string;
- try {
- const manifest = await accessApi.getInviteOnboarding(invite.token);
- snippet = buildAgentSnippet({
- onboardingTextUrl: absoluteUrl,
- connectionCandidates:
- manifest.onboarding.connectivity?.connectionCandidates ?? null,
- testResolutionUrl:
- manifest.onboarding.connectivity?.testResolutionEndpoint?.url ??
- null
- });
- } catch {
- snippet = buildAgentSnippet({
- onboardingTextUrl: absoluteUrl,
- connectionCandidates: null,
- testResolutionUrl: null
- });
- }
- setInviteSnippet(snippet);
- try {
- await navigator.clipboard.writeText(snippet);
- setSnippetCopied(true);
- setSnippetCopyDelightId((prev) => prev + 1);
- setTimeout(() => setSnippetCopied(false), 2000);
- } catch {
- /* clipboard may not be available */
- }
- queryClient.invalidateQueries({
- queryKey: queryKeys.sidebarBadges(selectedCompanyId!)
- });
- },
- onError: (err) => {
- setInviteError(
- err instanceof Error ? err.message : "Failed to create invite"
- );
- }
- });
-
const syncLogoState = (nextLogoUrl: string | null) => {
setLogoUrl(nextLogoUrl ?? "");
void queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
@@ -190,13 +124,6 @@ export function CompanySettings() {
clearLogoMutation.mutate();
}
- useEffect(() => {
- setInviteError(null);
- setInviteSnippet(null);
- setSnippetCopied(false);
- setSnippetCopyDelightId(0);
- }, [selectedCompanyId]);
-
const archiveMutation = useMutation({
mutationFn: ({
companyId,
@@ -438,84 +365,6 @@ export function CompanySettings() {
- {/* Invites */}
-
-
- Invites
-
-
-
-
- Generate an OpenClaw agent invite snippet.
-
-
-
-
-
-
- {inviteError && (
- {inviteError}
- )}
- {inviteSnippet && (
-
-
-
- OpenClaw Invite Prompt
-
- {snippetCopied && (
-
-
- Copied
-
- )}
-
-
-
-
-
-
-
-
- )}
-
-
-
{/* Import / Export */}
@@ -606,117 +455,3 @@ export function CompanySettings() {
);
}
-
-function buildAgentSnippet(input: AgentSnippetInput) {
- const candidateUrls = buildCandidateOnboardingUrls(input);
- const resolutionTestUrl = buildResolutionTestUrl(input);
-
- const candidateList =
- candidateUrls.length > 0
- ? candidateUrls.map((u) => `- ${u}`).join("\n")
- : "- (No candidate URLs available yet.)";
-
- const connectivityBlock =
- candidateUrls.length === 0
- ? `No candidate URLs are available. Ask your user to configure a reachable hostname in Paperclip, then retry.
-Suggested steps:
-- choose a hostname that resolves to the Paperclip host from your runtime
-- run: pnpm paperclipai allowed-hostname
-- restart Paperclip
-- verify with: curl -fsS http://:3100/api/health
-- regenerate this invite snippet`
- : `If none are reachable, ask your user to add a reachable hostname in Paperclip, restart, and retry.
-Suggested command:
-- pnpm paperclipai allowed-hostname
-Then verify with: curl -fsS /api/health`;
-
- const resolutionLine = resolutionTestUrl
- ? `\nYou MUST test Paperclip-to-gateway reachability, call: ${resolutionTestUrl}?url= (using the hostname that worked above). Do not assume your 172.x is necessarily reachable from Paperclip. Test it. `
- : "";
-
- return `You're invited to join a Paperclip organization.
-
-The URLs you should try are:
-${candidateList}
-
-Connectivity guidance:
-If you are running on a different machine than Paperclip, Paperclip must be reachable at one of the hostnames used above.
-Verify the hostname works from your runtime with: GET /api/health
-
-${connectivityBlock}
-
-For your "agentName", just use your own agent name (OpenClaw or whatever your name is) and send it directly.
-You also need to send a gateway URL that Paperclip can access. Check your config and include that URL.${resolutionLine}
-
----
-
-This onboarding flow is for OpenClaw Gateway.
-When you submit the join request, use:
-- \`adapterType: "openclaw_gateway"\`
-- \`agentDefaultsPayload.url\` as your \`ws://\` or \`wss://\` gateway URL
-- \`agentDefaultsPayload.headers["x-openclaw-token"]\` with your gateway token
-- (legacy accepted) \`agentDefaultsPayload.headers["x-openclaw-auth"]\`
-- Keep device auth enabled (recommended). If \`devicePrivateKeyPem\` is omitted, Paperclip will generate and persist one during join so pairing approvals remain stable.
-- Only use \`disableDeviceAuth=true\` for special environments where pairing cannot be completed.
-
-Do NOT use \`/v1/responses\` or \`/hooks/*\` in this join flow.
-
-Before you do anything, please respond to your user that you understand the instructions and you're going to work on them. Then do the step above in another session called "paperclip-onboarding" and then tell your user when you're done. Update your user in intermediate steps along the way so they know what's going on.
-
-Then after you've connected to Paperclip (exchanged keys etc.) you MUST review and follow the onboarding instructions in onboarding.txt they give you.
-
-`;
-}
-
-function buildCandidateOnboardingUrls(input: AgentSnippetInput): string[] {
- const candidates = (input.connectionCandidates ?? [])
- .map((candidate) => candidate.trim())
- .filter(Boolean);
- const urls = new Set();
- let onboardingUrl: URL | null = null;
-
- try {
- onboardingUrl = new URL(input.onboardingTextUrl);
- urls.add(onboardingUrl.toString());
- } catch {
- const trimmed = input.onboardingTextUrl.trim();
- if (trimmed) {
- urls.add(trimmed);
- }
- }
-
- if (!onboardingUrl) {
- for (const candidate of candidates) {
- urls.add(candidate);
- }
- return Array.from(urls);
- }
-
- const onboardingPath = `${onboardingUrl.pathname}${onboardingUrl.search}`;
- for (const candidate of candidates) {
- try {
- const base = new URL(candidate);
- urls.add(`${base.origin}${onboardingPath}`);
- } catch {
- urls.add(candidate);
- }
- }
-
- return Array.from(urls);
-}
-
-function buildResolutionTestUrl(input: AgentSnippetInput): string | null {
- const explicit = input.testResolutionUrl?.trim();
- if (explicit) return explicit;
-
- try {
- const onboardingUrl = new URL(input.onboardingTextUrl);
- const testPath = onboardingUrl.pathname.replace(
- /\/onboarding\.txt$/,
- "/test-resolution"
- );
- return `${onboardingUrl.origin}${testPath}`;
- } catch {
- return null;
- }
-}
diff --git a/ui/storybook/stories/dialogs-modals.stories.tsx b/ui/storybook/stories/dialogs-modals.stories.tsx
index 1d9e968d..e2836fd9 100644
--- a/ui/storybook/stories/dialogs-modals.stories.tsx
+++ b/ui/storybook/stories/dialogs-modals.stories.tsx
@@ -561,7 +561,7 @@ function IssueDialogOpener({
return ;
}
-function AgentDialogOpener({ advanced }: { advanced?: boolean }) {
+function AgentDialogOpener({ variant = "recommendation" }: { variant?: "recommendation" | "advanced" | "invite" }) {
const { openNewAgent } = useDialog();
useOpenWhenCompanyReady(() => {
@@ -569,12 +569,12 @@ function AgentDialogOpener({ advanced }: { advanced?: boolean }) {
});
useEffect(() => {
- if (!advanced) return undefined;
+ if (variant === "recommendation") return undefined;
const timer = window.setTimeout(() => {
- clickButtonByText("advanced configuration");
+ clickButtonByText(variant === "advanced" ? "Configure a runtime" : "Invite an external agent");
}, 250);
return () => window.clearTimeout(timer);
- }, [advanced]);
+ }, [variant]);
return ;
}
@@ -963,7 +963,21 @@ export const NewAgentAdapterSelection: Story = {
description="Advanced branch of the agent creation wizard showing registered adapter choices and recommended states."
badges={["populated", "adapters", "advanced"]}
>
-
+
+
+ ),
+};
+
+export const NewAgentExternalInvite: Story = {
+ name: "New Agent - External Invite",
+ render: () => (
+
+
),
};
|