History

Quick add pulls every knowledge source; rate the AI guidance

The GitHub registry row quick-added with no auth methods: its spec declares no security at all, the preset's OAuth endpoints only rode the preset path, and the registry's PAT header pattern was dropped. The OpenAPI quick add now pulls both across — a URL-matched preset contributes its OAuth template, spec overrides, spec format, family, and health check; the registry's header pattern becomes a declared API-key method — while spec-derived defaults still apply when neither source knows anything. The AI-generated guidance line also gains thumbs: votes land in analytics keyed by domain and credential label, so wrong registry guidance is findable instead of silently misleading.

Rhys Sullivan committed 2 months ago

Commit 6e3cd061 parent 797c4863 files · +124 −16Browse files

Unified · Split

3 files
  1. packages/plugins/openapi/src/react/integration-plugin.ts +57 −2
  2. packages/react/src/api/analytics.tsx +7 −0
  3. packages/react/src/components/credential-guidance.tsx +60 −14
packages/plugins/openapi/src/react/integration-plugin.ts+57−2
@@ -7,12 +7,33 @@ import type { · 6 unmodified lines
77 IntegrationQuickAddInput,
88 IntegrationQuickAddResult,
99 } from "@executor-js/sdk/client";
10+import { AuthTemplateSlug } from "@executor-js/sdk/shared";
1011 import { slugifyNamespace } from "@executor-js/react/plugins/integration-identity";
12+import { placementFromHeaderPattern } from "@executor-js/react/lib/auth-placements";
1113 import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
12−import { openApiPresets } from "../sdk/presets";
14+import { openApiPresets, type OpenApiPreset } from "../sdk/presets";
1315 import { addOpenApiSpec } from "./atoms";
16+import { openApiWireAuthInput, templateFromPlacements } from "./auth-method-config";
1417 import { decodeOpenApiSpecOverrides } from "../sdk/spec-overrides";
1518
19+const normalizedSpecUrl = (url: string): string => {
20+ if (!URL.canParse(url)) return url.trim().replace(/\/$/, "");
21+ const parsed = new URL(url);
22+ parsed.hash = "";
23+ parsed.searchParams.sort();
24+ return parsed.toString().replace(/\/$/, "");
25+};
26+
27+/** The preset table still knows things no spec can say — GitHub's OAuth
28+ * endpoints against a spec that declares NO security at all. A registry row
29+ * whose URL is a preset's URL gets that knowledge pulled across. */
30+const presetForSpecUrl = (url: string): OpenApiPreset | undefined => {
31+ const target = normalizedSpecUrl(url);
32+ return openApiPresets.find(
33+ (preset) => preset.url !== undefined && normalizedSpecUrl(preset.url) === target,
34+ );
35+};
36+
1637 /** One-click add for a registry OpenAPI row. The spec itself is the
1738 * configuration: omitting `authenticationTemplate` and `baseUrl` tells the
1839 * server to derive both from the document, exactly what the add page's
@@ -34,12 +55,46 @@ function useOpenApiQuickAdd(): ( · 15 unmodified lines
3455 if (input.specOverrides && input.specOverrides.length > 0 && specOverrides === undefined) {
3556 return { ok: false, reason: "unparseable spec overrides" };
3657 }
58+ const preset = presetForSpecUrl(input.url);
59+ // Declared methods travel from BOTH knowledge sources: the preset's
60+ // OAuth template (endpoints a spec cannot carry) and the registry's
61+ // header pattern (GitHub's spec declares no security at all, but the
62+ // registry knows a PAT goes in the Authorization header). When neither
63+ // knows anything, omitting the field lets the spec's own schemes derive.
64+ const presetMethods = (preset?.authTemplate ?? []).flatMap((template) =>
65+ template.kind === "oauth2"
66+ ? [
67+ openApiWireAuthInput({
68+ ...template,
69+ slug: AuthTemplateSlug.make(template.slug),
70+ resource: template.resource ?? undefined,
71+ }),
72+ ]
73+ : [],
74+ );
75+ const registryPlacement = input.authHeader
76+ ? placementFromHeaderPattern(input.authHeader)
77+ : null;
78+ const authenticationTemplate = [
79+ ...presetMethods,
80+ ...(registryPlacement
81+ ? [openApiWireAuthInput(templateFromPlacements([registryPlacement]))]
82+ : []),
83+ ];
84+ const presetOverrides =
85+ specOverrides && specOverrides.length > 0 ? specOverrides : preset?.specOverrides;
3786 const exit = await doAdd({
3887 payload: {
3988 spec: { kind: "url" as const, url: input.url },
4089 slug,
4190 name: input.name,
42− ...(specOverrides && specOverrides.length > 0 ? { specOverrides } : {}),
91+ ...(preset?.specFormat ? { specFormat: preset.specFormat } : {}),
92+ ...(preset?.family ? { family: preset.family } : {}),
93+ ...(preset?.healthCheck ? { healthCheck: preset.healthCheck } : {}),
94+ ...(presetOverrides && presetOverrides.length > 0
95+ ? { specOverrides: presetOverrides }
96+ : {}),
97+ ...(authenticationTemplate.length > 0 ? { authenticationTemplate } : {}),
4398 },
4499 reactivityKeys: integrationWriteKeys,
45100 });
packages/react/src/api/analytics.tsx+7−0
@@ -40,6 +40,13 @@ export interface AnalyticsEvents { · 39 unmodified lines
4040 detected_kind?: string;
4141 confidence?: string;
4242 };
43+ /** Thumbs on the AI-generated credential guidance panel — the accuracy
44+ * signal for the registry's machine-written setup text. */
45+ credential_guidance_rated: {
46+ domain: string;
47+ credential_label: string;
48+ vote: "up" | "down";
49+ };
4350 integration_add_started: {
4451 plugin_key: string;
4552 via: "detect" | "manual" | "preset" | "command_palette" | "catalog";
packages/react/src/components/credential-guidance.tsx+60−14
@@ -1,8 +1,10 @@
1−import { useMemo } from "react";
2−import { ArrowUpRightIcon } from "lucide-react";
1+import { useMemo, useState } from "react";
2+import { ArrowUpRightIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react";
33 import { getDomain } from "tldts";
44
5+import { trackEvent } from "../api/analytics";
56 import { useCredentialGuidance } from "../lib/integrations-sh-catalog";
7+import { cn } from "../lib/utils";
68
79 // ---------------------------------------------------------------------------
810 // What this key is, and where to get one.
@@ -105,18 +107,62 @@ export function CredentialGuidancePanel(props: { · 96 unmodified lines
105107 {relevant.setup ? <SetupText text={relevant.setup} /> : null}
106108 {/* Provenance, stated plainly: this text is machine-written registry
107109 data, not the provider's own docs — the reader should weight it
108− accordingly (menus move, prefixes change). */}
109− <p className="pt-0.5 font-mono text-[10px] uppercase tracking-wide text-muted-foreground/60">
110− AI-generated guidance via{" "}
111− <a
112− href={`https://integrations.sh/${domain ?? ""}`}
113− target="_blank"
114− rel="noreferrer"
115− className="underline underline-offset-2 hover:text-muted-foreground"
116− >
117− integrations.sh
118− </a>
119− </p>
110+ accordingly (menus move, prefixes change). The thumbs are the
111+ accuracy loop: votes land in analytics keyed by domain, so wrong
112+ guidance is findable instead of silently misleading. */}
113+ <div className="flex items-center justify-between gap-3 pt-0.5">
114+ <p className="font-mono text-[10px] uppercase tracking-wide text-muted-foreground/60">
115+ AI-generated guidance via{" "}
116+ <a
117+ href={`https://integrations.sh/${domain ?? ""}`}
118+ target="_blank"
119+ rel="noreferrer"
120+ className="underline underline-offset-2 hover:text-muted-foreground"
121+ >
122+ integrations.sh
123+ </a>
124+ </p>
125+ <GuidanceVote domain={domain ?? ""} label={relevant.label} />
126+ </div>
120127 </div>
121128 );
122129 }
130+
131+/** One vote per render of the panel; the pair collapses to the chosen thumb
132+ * so a second opinion means reopening, not flip-flopping the metric. */
133+function GuidanceVote(props: { readonly domain: string; readonly label: string }) {
134+ const [voted, setVoted] = useState<"up" | "down" | null>(null);
135+ const vote = (value: "up" | "down") => {
136+ if (voted) return;
137+ setVoted(value);
138+ trackEvent("credential_guidance_rated", {
139+ domain: props.domain,
140+ credential_label: props.label,
141+ vote: value,
142+ });
143+ };
144+ return (
145+ <span className="flex shrink-0 items-center gap-1">
146+ {(["up", "down"] as const).map((value) => {
147+ const Icon = value === "up" ? ThumbsUpIcon : ThumbsDownIcon;
148+ if (voted && voted !== value) return null;
149+ return (
150+ // oxlint-disable-next-line react/forbid-elements -- an icon chip, not a Button variant
151+ <button
152+ key={value}
153+ type="button"
154+ aria-label={value === "up" ? "Guidance was accurate" : "Guidance was wrong"}
155+ disabled={voted !== null}
156+ onClick={() => vote(value)}
157+ className={cn(
158+ "rounded p-1 text-muted-foreground/50 transition-colors",
159+ voted === value ? "text-foreground" : "hover:bg-muted hover:text-muted-foreground",
160+ )}
161+ >
162+ <Icon className="size-3" aria-hidden />
163+ </button>
164+ );
165+ })}
166+ </span>
167+ );
168+}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t