Skip to content
Merged
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
36 changes: 30 additions & 6 deletions app/_components/copy-page-override.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ const COPYING_TEXT = "Copying\u2026";
const COPY_FAILED_TEXT = "Failed to copy";
const DROPDOWN_IDENTIFIER = "Markdown for LLMs";

// A toolkit reference page: /<locale>/resources/integrations/<category>/<slug>.
// Captures the slug so we can pull full markdown from the data route.
const TOOLKIT_PAGE_PATH =
/^\/[^/]+\/resources\/integrations\/[^/]+\/([^/]+)\/?$/;

const ICON_COPY = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16"><rect x="9" y="9" width="13" height="13" rx="2"></rect><path d="M5 15H4C2.89543 15 2 14.1046 2 13V4C2 2.89543 2.89543 2 4 2H13C14.1046 2 15 2.89543 15 4V5"></path></svg>`;

const ICON_SPINNER = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" class="x:animate-spin"><circle cx="12" cy="12" r="10" stroke-opacity="0.25"></circle><path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round"></path></svg>`;
Expand Down Expand Up @@ -84,15 +89,34 @@ export function CopyPageOverride() {

const fetchAndCopyMarkdown = useCallback(async (): Promise<boolean> => {
try {
const response = await fetch(pathname, {
headers: { Accept: "text/markdown" },
});
let markdown: string | null = null;

// Toolkit reference pages render per-tool detail client-only, so the edge
// HTML→markdown view would miss parameters/output/examples. Pull full
// markdown from the data route instead. If the slug isn't a generated
// toolkit (e.g. a static partner page) the route 404s and we fall back to
// the normal page fetch below.
const toolkitSlug = pathname.match(TOOLKIT_PAGE_PATH)?.[1];
if (toolkitSlug) {
const dataResponse = await fetch(
`/api/toolkit-data/${encodeURIComponent(toolkitSlug)}`,
{ headers: { Accept: "text/markdown" } }
);
if (dataResponse.ok) {
markdown = await dataResponse.text();
}
}

if (!response.ok) {
throw new Error(`Failed to fetch markdown: ${response.status}`);
if (markdown === null) {
const response = await fetch(pathname, {
headers: { Accept: "text/markdown" },
});
if (!response.ok) {
throw new Error(`Failed to fetch markdown: ${response.status}`);
}
markdown = await response.text();
}

const markdown = await response.text();
await navigator.clipboard.writeText(markdown);
return true;
} catch {
Expand Down
118 changes: 83 additions & 35 deletions app/_components/scope-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button } from "@arcadeai/design-system";
import { Check, Copy, KeyRound, ShieldCheck, Wrench } from "lucide-react";
import posthog from "posthog-js";
import { useCallback, useEffect, useMemo, useState } from "react";
import { loadToolkitDetail } from "./toolkit-docs/components/use-toolkit-detail";

const COPY_FEEDBACK_MS = 2000;

Expand Down Expand Up @@ -47,20 +48,33 @@ type ScopePickerProps = {
tools: Tool[];
selectedTools?: string[];
onSelectedToolsChange?: (selectedTools: string[]) => void;
/** Toolkit id — lets "Copy tools JSON" lazily fetch full per-tool detail. */
toolkitId?: string;
};

function CopyButton({ text, label }: { text: string; label: string }) {
function CopyButton({
text,
getText,
label,
}: {
text?: string;
// Build the text to copy on demand (e.g. lazily fetch full tool detail).
getText?: () => Promise<string>;
label: string;
}) {
const [copied, setCopied] = useState(false);

const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(text);
await navigator.clipboard.writeText(
getText ? await getText() : (text ?? "")
);
setCopied(true);
setTimeout(() => setCopied(false), COPY_FEEDBACK_MS);
} catch {
// Ignore clipboard errors (e.g., permissions, unsupported browser).
}
}, [text]);
}, [text, getText]);

return (
<Button onClick={handleCopy} size="sm" title={label} variant="outline">
Expand All @@ -74,6 +88,50 @@ function CopyButton({ text, label }: { text: string; label: string }) {
);
}

type ToolDetailMap = Awaited<ReturnType<typeof loadToolkitDetail>>;

/**
* Build the "selected tools" JSON. When full per-tool detail is available
* (fetched lazily on copy), emit the full definition; otherwise fall back to a
* basic shape — but always key on the qualified name so the identifier stays
* correct for downstream tool configs.
*/
function buildSelectedToolsJson(
tools: Tool[],
selectedToolsSet: Set<string>,
detailMap?: ToolDetailMap
): string {
const selected = tools.filter((tool) => selectedToolsSet.has(tool.name));
return JSON.stringify(
selected.map((tool) => {
const detail = tool.qualifiedName
? detailMap?.get(tool.qualifiedName)
: undefined;
const parameters = detail?.parameters ?? tool.parameters;
const name = tool.qualifiedName ?? tool.name;
if (parameters) {
return {
name,
description: detail?.description ?? tool.description ?? null,
parameters: parameters.map((p) => ({
name: p.name,
type: p.type,
required: p.required,
description: p.description,
...(p.enum ? { enum: p.enum } : {}),
})),
scopes: tool.scopes,
secrets: tool.secrets ?? [],
output: detail?.output ?? tool.output ?? null,
};
}
return { name, scopes: tool.scopes, secrets: tool.secrets ?? [] };
}),
null,
JSON_PRETTY_PRINT_INDENT
);
}

export function getSelectedToolNames(
tools: Tool[],
selectedTools: Set<string>
Expand Down Expand Up @@ -259,6 +317,7 @@ export default function ScopePicker({
tools,
selectedTools,
onSelectedToolsChange,
toolkitId,
}: ScopePickerProps) {
const [internalSelectedTools, setInternalSelectedTools] = useState<
Set<string>
Expand Down Expand Up @@ -353,37 +412,6 @@ export default function ScopePicker({
const scopesAsText = requiredScopes.join("\n");
const secretsAsText = requiredSecrets.join("\n");
const toolNamesAsText = selectedToolNames.join(", ");
const selectedToolsAsJson = JSON.stringify(
tools
.filter((t) => selectedToolsSet.has(t.name))
.map((t) => {
// If full tool definition is available, include all fields
if (t.qualifiedName && t.parameters) {
return {
name: t.qualifiedName,
description: t.description ?? null,
parameters: t.parameters.map((p) => ({
name: p.name,
type: p.type,
required: p.required,
description: p.description,
...(p.enum ? { enum: p.enum } : {}),
})),
scopes: t.scopes,
secrets: t.secrets ?? [],
output: t.output ?? null,
};
}
// Fallback to basic format
return {
name: t.name,
scopes: t.scopes,
secrets: t.secrets ?? [],
};
}),
null,
JSON_PRETTY_PRINT_INDENT
);

return (
<div className="my-6 overflow-hidden rounded-xl bg-neutral-dark/20">
Expand Down Expand Up @@ -532,7 +560,27 @@ export default function ScopePicker({
{selectedToolNames.length > 0 && (
<div className="mb-4 flex flex-wrap gap-2">
<CopyButton label="Copy tool names" text={toolNamesAsText} />
<CopyButton label="Copy tools JSON" text={selectedToolsAsJson} />
<CopyButton
getText={async () => {
// Pull full per-tool detail (parameters/output) on demand — the
// page payload only carries the summary — so the JSON keeps full
// fidelity; fall back to the summary if the fetch fails.
let detailMap: ToolDetailMap | undefined;
if (toolkitId) {
try {
detailMap = await loadToolkitDetail(toolkitId);
} catch {
// Keep detailMap undefined → summary-only JSON.
}
}
return buildSelectedToolsJson(
tools,
selectedToolsSet,
detailMap
);
}}
label="Copy tools JSON"
/>
{showAdvanced && requiredScopes.length > 0 && (
<CopyButton label="Copy scopes" text={scopesAsText} />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { useEffect, useMemo, useRef, useState } from "react";

import { SCROLLING_CELL } from "../constants";
import { splitEmails } from "../lib/neutralize-emails";
import type {
AvailableToolsTableProps,
BehaviorFlagKey,
Expand Down Expand Up @@ -585,7 +586,11 @@ function AvailableToolsRow({
</td>
<td className="max-w-[300px] px-4 py-3.5 text-sm text-foreground">
<ScrollingCell>
<span>{tool.description ?? "No description provided."}</span>
<span>
{tool.description
? splitEmails(tool.description)
: "No description provided."}
</span>
</ScrollingCell>
</td>
<td className="px-4 py-3.5">
Expand Down
Loading
Loading