Skip to content

Commit

The Integrations directory shows each tool's own mark, and only what g1t sets up end to end can be set up: cards draw the brand's icon from Simple Icons where it has one (GitHub, Linear, Jira, Sentry, Datadog, Anthropic, Gemini and some sixty more), in the brand's colour washed toward the text colour, and in the text colour itself when the brand's is black or nearly (GitHub, Notion, Vercel), so each reads in the dark theme and the light one; outgoing webhooks take the webhook mark, and the rest keep their letters, which now count letters and digits alone, so "GitHub (MCP)" is GM and no longer "G(". The search's magnifying glass no longer sits on the placeholder: the field is the input group used elsewhere, with the icon as its addon (the older Input it used dropped the padding it was given). Every connector backed only by an MCP server (GitHub (MCP), Linear (MCP), Notion, Atlassian, Sentry (MCP), Cloudflare, Vercel, Stripe, Figma and the rest) is Soon, since Customize, where they were connected, went with the hosted agents and their Set up led nowhere; they sort after what works, with Ask for this in place of Set up, and the rail cannot pin them. Available stays GitHub, webhooks, Linear, Jira, Sentry, Datadog, the alerts webhook and the AI providers for a workspace, and GitHub sign-in and MCP clients for you, as the docs' Available today already said; a test holds the catalog to that list.

syntaqxcommitted Parent85c083aBrowse files
6 files+270−610/6 viewed
+73−48
2525 import { type ConnectorScope, type ConnectorView, connectorPath } from "@g1t/contracts/connectors";
2626
2727 import { AI_PROVIDERS } from "../lib/ai-providers";
28+import { type BrandIcon, brandIcon } from "../lib/brand-icons";
2829 import { CONTACT } from "../lib/legal";
2930 import {
3031 type CategoryFilter,
4243 import { cn } from "../lib/cn";
4344 import { integrationsSection } from "../lib/integration-sections";
4445 import { Badge } from "./ui/badge";
46+import { Button } from "./ui/button";
4547 import { Hint } from "./ui/hint";
46−import { Input } from "./ui";
48+import { Input, InputAddon, InputGroup } from "./ui/input";
4749 import { Card } from "./ui/card";
4850
4951 const CATEGORY_ICONS: Record<CategoryFilter, ReactNode> = {
7173 linear: "#8b8ff0",
7274 };
7375
74−/** A provider's mark: its initials in its colour, or an icon for the generic ones. */
75−export function ProviderMark({ provider, size = 32 }: { provider: Provider; size?: number }) {
76− const entry = AI_PROVIDERS[provider];
77− const color = entry?.color ?? PROVIDER_COLORS[provider] ?? "#a1a1aa";
78− const content =
79− provider === "webhook" ? (
80− <Webhook size={size * 0.5} />
81− ) : provider.endsWith("_endpoint") ? (
82− <Bot size={size * 0.5} />
83− ) : (
84− entry?.mark || PROVIDERS[provider].label[0]
85− );
76+/**
77+ * A mark's tile: a brand's icon, or letters or an icon, on a wash of
78+ * `color`. With no colour (a brand drawn in black) it is drawn in the text
79+ * colour on a neutral wash, so it reads in both themes.
80+ */
81+function MarkTile({ color, size, children }: { color: string | null; size: number; children: ReactNode }) {
82+ const tint = color ?? "var(--color-fg)";
8683 return (
8784 <span
8885 aria-hidden="true"
9188 width: size,
9289 height: size,
9390 borderRadius: size * 0.28,
94− fontSize: size * (String(content).length > 1 ? 0.34 : 0.42),
95− background: `color-mix(in srgb, ${color} 16%, transparent)`,
96− // Toward the text colour, so the letters read on the tile in both themes.
97− color: `color-mix(in srgb, ${color} 60%, var(--color-fg))`,
98− boxShadow: `inset 0 0 0 1px color-mix(in srgb, ${color} 30%, transparent)`,
91+ fontSize: size * (typeof children === "string" && children.length > 1 ? 0.34 : 0.42),
92+ background: `color-mix(in srgb, ${tint} ${color ? 16 : 8}%, transparent)`,
93+ // Toward the text colour, so the mark reads on the tile in both themes.
94+ color: color ? `color-mix(in srgb, ${color} 60%, var(--color-fg))` : "var(--color-fg)",
95+ boxShadow: `inset 0 0 0 1px color-mix(in srgb, ${tint} ${color ? 30 : 14}%, transparent)`,
9996 }}
10097 >
101− {content}
98+ {children}
10299 </span>
103100 );
104101 }
105102
103+/** A brand's icon, from Simple Icons, in the tile's colour. */
104+function BrandGlyph({ icon, size }: { icon: BrandIcon; size: number }) {
105+ return (
106+ <svg viewBox="0 0 24 24" width={size * 0.5} height={size * 0.5} fill="currentColor">
107+ <path d={icon.path} />
108+ </svg>
109+ );
110+}
111+
112+/** A provider's mark: its brand's icon, an icon for the generic ones, or its initials in its colour. */
113+export function ProviderMark({ provider, size = 32 }: { provider: Provider; size?: number }) {
114+ const brand = provider === "webhook" ? null : brandIcon(provider.replace(/_/g, "-"));
115+ if (brand) {
116+ return (
117+ <MarkTile color={brand.color} size={size}>
118+ <BrandGlyph icon={brand} size={size} />
119+ </MarkTile>
120+ );
121+ }
122+ const entry = AI_PROVIDERS[provider];
123+ const content =
124+ provider === "webhook" ? (
125+ <Webhook size={size * 0.5} />
126+ ) : provider.endsWith("_endpoint") ? (
127+ <Bot size={size * 0.5} />
128+ ) : (
129+ entry?.mark || monogram(PROVIDERS[provider].label)
130+ );
131+ return (
132+ <MarkTile color={entry?.color ?? PROVIDER_COLORS[provider] ?? "#a1a1aa"} size={size}>
133+ {content}
134+ </MarkTile>
135+ );
136+}
137+
106138 /** Providers to choose from, as compact tiles that open their form. */
107139 export function ProviderTiles({
108140 providers,
140172 );
141173 }
142174
143−/** A connector's tile: its provider's mark, or its letters on a steady colour. No logos. */
175+/** A connector's tile: its brand's icon, its provider's mark, or its letters on a steady colour. */
144176 export function ConnectorMark({ view, size = 36 }: { view: Pick<ConnectorView, "id" | "name" | "provider">; size?: number }) {
177+ const brand = brandIcon(view.id);
178+ if (brand) {
179+ return (
180+ <MarkTile color={brand.color} size={size}>
181+ <BrandGlyph icon={brand} size={size} />
182+ </MarkTile>
183+ );
184+ }
145185 if (view.provider) return <ProviderMark provider={view.provider} size={size} />;
146− const color = tileColor(view.id);
147− const letters = monogram(view.name);
186+ // Outgoing webhooks have no brand: the same webhook mark as incoming ones.
187+ if (view.id === "webhooks") return <ProviderMark provider="webhook" size={size} />;
148188 return (
149− <span
150− aria-hidden="true"
151− className="inline-flex shrink-0 items-center justify-center font-semibold tracking-tight"
152− style={{
153− width: size,
154− height: size,
155− borderRadius: size * 0.28,
156− fontSize: size * (letters.length > 1 ? 0.34 : 0.42),
157− background: `color-mix(in srgb, ${color} 16%, transparent)`,
158− // Toward the text colour, so the letters read in both themes.
159− color: `color-mix(in srgb, ${color} 60%, var(--color-fg))`,
160− boxShadow: `inset 0 0 0 1px color-mix(in srgb, ${color} 30%, transparent)`,
161− }}
162− >
163− {letters}
164− </span>
189+ <MarkTile color={tileColor(view.id)} size={size}>
190+ {monogram(view.name)}
191+ </MarkTile>
165192 );
166193 }
167194
258285 </nav>
259286
260287 <div className="mt-5 min-w-0 md:mt-0">
261− <div className="relative">
262− <Search size={15} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
288+ <InputGroup>
289+ <InputAddon className="pr-0">
290+ <Search size={15} />
291+ </InputAddon>
263292 <Input
264293 type="search"
265294 value={query}
266295 onChange={(event) => setQuery(event.target.value)}
267296 placeholder={category === "all" ? "Search integrations" : `Search all integrations, not just ${categoryTitle(category)}`}
268297 aria-label="Search integrations"
269− className="pl-9"
270298 />
271− </div>
299+ </InputGroup>
272300
273301 {empty && (
274302 <Card asChild tone="plain" className="mt-8 border-dashed px-4 py-8 text-center text-sm text-muted">
423451 <div className="mt-auto flex items-end justify-between gap-3 pt-3">
424452 <p className="text-[0.6875rem] text-faint">{also}</p>
425453 {soon ? (
426− <a
427− href={askHref({ connector: view, scope, workspace, address: CONTACT.support })}
428− className="relative z-10 shrink-0 rounded-md border border-line px-2.5 py-1 text-xs font-medium text-fg/80 hover:border-line-strong hover:bg-raised hover:text-fg"
429− >
430− Ask for this
431− </a>
454+ <Button asChild variant="outline" size="xs" className="relative z-10">
455+ <a href={askHref({ connector: view, scope, workspace, address: CONTACT.support })}>Ask for this</a>
456+ </Button>
432457 ) : to ? (
433458 <span className="flex shrink-0 items-center gap-0.5 text-xs font-medium text-accent">
434459 {state ? (canManage ? "Manage" : "View") : canManage ? "Set up" : "View"}
+162−0
1+/**
2+ * Connectors' brand marks, from Simple Icons, by connector id
3+ * (@g1t/contracts/connectors). A connector without one here keeps its
4+ * letters. Named imports, so only these icons are bundled.
5+ */
6+import {
7+ type SimpleIcon,
8+ si1password,
9+ siAnthropic,
10+ siAsana,
11+ siAtlassian,
12+ siBetterstack,
13+ siBitbucket,
14+ siBox,
15+ siBuildkite,
16+ siCircleci,
17+ siClickup,
18+ siCloudflare,
19+ siCoda,
20+ siDatadog,
21+ siDeepseek,
22+ siDiscord,
23+ siDocker,
24+ siDropbox,
25+ siFigma,
26+ siFlydotio,
27+ siGithub,
28+ siGitlab,
29+ siGmail,
30+ siGooglebigquery,
31+ siGooglecalendar,
32+ siGooglecloud,
33+ siGoogledocs,
34+ siGoogledrive,
35+ siGooglegemini,
36+ siGooglemeet,
37+ siGrafana,
38+ siHubspot,
39+ siHuggingface,
40+ siIntercom,
41+ siJira,
42+ siKubernetes,
43+ siLinear,
44+ siMiro,
45+ siMistralai,
46+ siModelcontextprotocol,
47+ siNeon,
48+ siNetlify,
49+ siNewrelic,
50+ siNotion,
51+ siOkta,
52+ siOpenrouter,
53+ siOpsgenie,
54+ siPagerduty,
55+ siPaypal,
56+ siPostgresql,
57+ siPosthog,
58+ siSentry,
59+ siShortcut,
60+ siSnowflake,
61+ siSnyk,
62+ siStorybook,
63+ siStripe,
64+ siSupabase,
65+ siTrello,
66+ siVercel,
67+ siZapier,
68+ siZendesk,
69+ siZoom,
70+} from "simple-icons";
71+
72+const BRAND_ICONS: Record<string, SimpleIcon> = {
73+ github: siGithub,
74+ "github-mcp": siGithub,
75+ gitlab: siGitlab,
76+ bitbucket: siBitbucket,
77+ circleci: siCircleci,
78+ buildkite: siBuildkite,
79+ "docker-hub": siDocker,
80+ linear: siLinear,
81+ "linear-mcp": siLinear,
82+ jira: siJira,
83+ asana: siAsana,
84+ shortcut: siShortcut,
85+ clickup: siClickup,
86+ trello: siTrello,
87+ discord: siDiscord,
88+ zoom: siZoom,
89+ "google-meet": siGooglemeet,
90+ "google-calendar": siGooglecalendar,
91+ gmail: siGmail,
92+ notion: siNotion,
93+ atlassian: siAtlassian,
94+ "google-docs": siGoogledocs,
95+ zapier: siZapier,
96+ coda: siCoda,
97+ sentry: siSentry,
98+ "sentry-mcp": siSentry,
99+ datadog: siDatadog,
100+ pagerduty: siPagerduty,
101+ opsgenie: siOpsgenie,
102+ grafana: siGrafana,
103+ "new-relic": siNewrelic,
104+ "better-stack": siBetterstack,
105+ cloudflare: siCloudflare,
106+ "cloudflare-docs": siCloudflare,
107+ "google-cloud": siGooglecloud,
108+ vercel: siVercel,
109+ netlify: siNetlify,
110+ "fly-io": siFlydotio,
111+ kubernetes: siKubernetes,
112+ anthropic: siAnthropic,
113+ gemini: siGooglegemini,
114+ mistral: siMistralai,
115+ deepseek: siDeepseek,
116+ openrouter: siOpenrouter,
117+ mcp: siModelcontextprotocol,
118+ "hugging-face": siHuggingface,
119+ postgresql: siPostgresql,
120+ snowflake: siSnowflake,
121+ bigquery: siGooglebigquery,
122+ posthog: siPosthog,
123+ supabase: siSupabase,
124+ neon: siNeon,
125+ zendesk: siZendesk,
126+ intercom: siIntercom,
127+ hubspot: siHubspot,
128+ stripe: siStripe,
129+ paypal: siPaypal,
130+ figma: siFigma,
131+ miro: siMiro,
132+ storybook: siStorybook,
133+ okta: siOkta,
134+ "1password": si1password,
135+ snyk: siSnyk,
136+ "google-drive": siGoogledrive,
137+ dropbox: siDropbox,
138+ box: siBox,
139+};
140+
141+/** A connector's brand mark: its SVG path, and the colour to draw it in. */
142+export type BrandIcon = { path: string; title: string; color: string | null };
143+
144+/** Relative luminance of a `RRGGBB` colour, 0 (black) to 1 (white). */
145+function luminance(hex: string): number {
146+ const [r, g, b] = [0, 2, 4].map((at) => {
147+ const channel = parseInt(hex.slice(at, at + 2), 16) / 255;
148+ return channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
149+ });
150+ return 0.2126 * r! + 0.7152 * g! + 0.0722 * b!;
151+}
152+
153+/**
154+ * The brand mark for connector `id`, or null. `color` is the brand's own,
155+ * or null when it is black or nearly (GitHub, Notion, Vercel): those are
156+ * drawn in the text colour, so they read on the dark theme as on the light.
157+ */
158+export function brandIcon(id: string): BrandIcon | null {
159+ const icon = BRAND_ICONS[id];
160+ if (!icon) return null;
161+ return { path: icon.path, title: icon.title, color: luminance(icon.hex) < 0.04 ? null : `#${icon.hex}` };
162+}
+5−1
4444
4545 test("search finds by name, description, tag and keyword, across every category", () => {
4646 const found = arrange(workspace, { category: "design", query: "sentry", connected: none });
47− assert.deepEqual(found.available.map((view) => view.id), ["sentry", "sentry-mcp"], "a search is not held to the category");
47+ assert.deepEqual(found.available.map((view) => view.id), ["sentry"], "a search is not held to the category");
48+ assert.deepEqual(found.soon.map((view) => view.id), ["sentry-mcp"], "what is coming sorts after what works");
4849 assert.ok(matchesQuery(personal.find((view) => view.id === "google-calendar")!, "in a meeting"));
4950 assert.ok(matchesQuery(workspace.find((view) => view.id === "anthropic")!, "claude"));
5051 assert.ok(matchesQuery(workspace.find((view) => view.id === "incident-io")!, "incidentio"));
7475 assert.equal(monogram("Sentry"), "S");
7576 assert.equal(monogram("incident.io"), "II");
7677 assert.equal(monogram("1Password"), "1P");
78+ assert.equal(monogram("GitHub (MCP)"), "GM", "punctuation is not a letter");
79+ assert.equal(monogram("Linear (MCP)"), "LM");
80+ assert.equal(monogram("Atlassian (Jira and Confluence)"), "AJ");
7781 assert.equal(tileColor("figma"), tileColor("figma"));
7882 });
7983
+6−2
101101 })).filter((group) => group.views.length > 0);
102102 }
103103
104−/** The letters on a connector's tile: two words' initials, or a name's capitals ("PagerDuty" → "PD"). */
104+/**
105+ * The letters on a connector's tile: two words' initials, or a name's
106+ * capitals ("PagerDuty" → "PD"). Only letters and digits count, so
107+ * "GitHub (MCP)" is "GM", never "G(".
108+ */
105109 export function monogram(name: string): string {
106− const words = name.split(/[\s.\-/]+/).filter((word) => /[A-Za-z0-9]/.test(word));
110+ const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
107111 if (words.length >= 2) return (words[0]![0]! + words[1]![0]!).toUpperCase();
108112 const word = words[0] ?? name;
109113 const capitals = word.slice(1).match(/[A-Z0-9]/);
+16−0
3737 }
3838 });
3939
40+test("only what g1t sets up end to end is available; the docs' Available today lists the same", () => {
41+ const available = (scope: "workspace" | "personal") =>
42+ connectorsFor(scope)
43+ .filter((view) => view.status === "available")
44+ .map((view) => view.id)
45+ .sort();
46+ const models = Object.entries(PROVIDERS)
47+ .filter(([, provider]) => provider.kind === "models")
48+ .map(([id]) => id.replace(/_/g, "-"));
49+ assert.deepEqual(available("workspace"), ["alerts-webhook", "datadog", "github", "jira", "linear", "sentry", "webhooks", ...models].sort());
50+ assert.deepEqual(available("personal"), ["github", "mcp"]);
51+ for (const connector of CONNECTORS) {
52+ if (connector.mcp) assert.equal(connector.status, "soon", `${connector.id}: nothing connects to MCP servers yet`);
53+ }
54+});
55+
4056 test("a connector shows its personal side to you", () => {
4157 const github = CONNECTORS.find((c) => c.id === "github")!;
4258 const mine = connectorView(github, "personal")!;
+8−10
7373 /** More words search should find it by. */
7474 keywords?: string[];
7575 /**
76− * The remote MCP server behind it, when the tool publishes one: g1t
77− * connects to it (docs.g1t.sh/guides/customize/, "Connectors"), signs in
78− * the way it asks (`oauth`: the browser; `header`: a key an owner
79− * pastes; `none`), lists its tools, and gives them to agents.
76+ * The remote MCP server behind it, when the tool publishes one, and how
77+ * it signs in (`oauth`: the browser; `header`: a key an owner pastes;
78+ * `none`). Its connector is soon: see `mcp()` below.
8079 */
8180 mcp?: {
8281 url: string;
141140 keywords: ["model", "llm", ...keywords],
142141 };
143142 }
144−
145−/** Where an MCP-backed connector is connected: Customize's Connectors page, with the directory entry chosen. */
146−export const MCP_CONNECT_PATH = "/:workspace/-/customize/connectors/new?directory=";
147143
148144 /**
149− * A tool with a remote MCP server: available now, connected from Customize
150− * (an owner signs in or pastes a key), its tools then given to agents.
145+ * A tool with a remote MCP server. Soon: Customize, where an owner
146+ * connected one, was retired with g1t's hosted agents, so nothing in g1t
147+ * connects to these servers today. The server is kept for when something
148+ * does; until then each person's own harness connects to it directly.
151149 */
152150 function mcp(
153151 id: string,
158156 capabilities: ConnectorCapability[] = ["Agents can read", "Agents can act"],
159157 keywords: string[] = [],
160158 ): Connector {
161− return { id, name, category, description, scopes: W, status: "available", href: { workspace: `${MCP_CONNECT_PATH}${id}` }, capabilities, keywords: ["mcp", ...keywords], mcp: server };
159+ return { id, name, category, description, scopes: W, status: "soon", capabilities, keywords: ["mcp", ...keywords], mcp: server };
162160 }
163161
164162 /** A planned connector. */