Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

148 linesCodeBlameRaw
1import { Check, Copy, ExternalLink } from "lucide-react";
2import { type KeyboardEvent, useId, useMemo, useState, useSyncExternalStore } from "react";
3import { useRouteLoaderData } from "react-router";
4
5import type { loader as rootLoader } from "../root";
6
7import {
8 AGENT_IDS,
9 type AgentId,
10 DEFAULT_AGENT,
11 agentsFor,
12 cursorInstallLink,
13 getAgentChoice,
14 setAgentChoice,
15 subscribeAgentChoice,
16} from "../lib/agent-setup";
17import { useAddresses } from "../lib/addresses";
18import { cn } from "../lib/cn";
19import { Button } from "./ui/button";
20import { Card } from "./ui/card";
21
22/** The agent this browser chose last; every toggle on the page shares it. */
23export function useAgentChoice(): [AgentId, (id: AgentId) => void] {
24 const root = useRouteLoaderData<typeof rootLoader>("root");
25 const fallback = root?.agentDefault ?? DEFAULT_AGENT;
26 const id = useSyncExternalStore(subscribeAgentChoice, () => getAgentChoice(fallback), () => root?.agentChoice ?? fallback);
27 return [id, setAgentChoice];
28}
29
30/**
31 * How to connect g1t to a coding agent: a tab per agent, the command or
32 * config for the one chosen, and a copy button. Choosing an agent switches
33 * every block on the page and is remembered.
34 */
35export function AgentSetup({ hint = true, className }: { hint?: boolean; className?: string }) {
36 const [chosen, choose] = useAgentChoice();
37 const [copied, setCopied] = useState(false);
38 const base = useId();
39 const { mcp } = useAddresses();
40 const agents = useMemo(() => agentsFor(mcp), [mcp]);
41 const agent = agents[chosen];
42
43 const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
44 const edge = event.key === "Home" ? 0 : event.key === "End" ? AGENT_IDS.length - 1 : null;
45 const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
46 if (!step && edge === null) return;
47 event.preventDefault();
48 const next = AGENT_IDS[edge ?? (AGENT_IDS.indexOf(chosen) + step + AGENT_IDS.length) % AGENT_IDS.length];
49 choose(next);
50 document.getElementById(`${base}-${next}`)?.focus();
51 };
52
53 const copy = () => {
54 void navigator.clipboard?.writeText(agent.code).then(
55 () => {
56 setCopied(true);
57 setTimeout(() => setCopied(false), 1500);
58 },
59 () => undefined,
60 );
61 };
62
63 return (
64 <div className={className}>
65 <Card radius="lg" className="@container overflow-hidden">
66 <div
67 role="tablist"
68 aria-label="AI client"
69 onKeyDown={onKeyDown}
70 className="flex flex-wrap items-center gap-0.5 border-b border-line bg-bg/40 px-1.5 py-1.5"
71 >
72 {AGENT_IDS.map((id) => {
73 const on = id === chosen;
74 return (
75 <button
76 key={id}
77 id={`${base}-${id}`}
78 type="button"
79 role="tab"
80 aria-selected={on}
81 aria-controls={`${base}-panel`}
82 tabIndex={on ? 0 : -1}
83 onClick={() => { setCopied(false); choose(id); }}
84 className={cn(
85 "shrink-0 rounded-md px-1.5 py-1 text-xs @sm:px-2.5 font-medium whitespace-nowrap transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent/50",
86 on ? "bg-raised text-fg" : "text-muted hover:text-fg",
87 )}
88 >
89 {agents[id].label}
90 </button>
91 );
92 })}
93 </div>
94 <div id={`${base}-panel`} role="tabpanel" aria-labelledby={`${base}-${chosen}`} className="relative">
95 {agent.file && (
96 <p className="px-3.5 pt-2.5 font-mono text-[0.6875rem] text-faint">{agent.file}</p>
97 )}
98 <pre className="overflow-x-auto py-2.5 pr-11 pl-3.5 font-mono text-[0.8125rem] leading-relaxed">
99 <code>
100 {agent.lang === "sh"
101 ? agent.code.split("\n").map((line, index) => (
102 <span key={index} className="block whitespace-pre-wrap wrap-anywhere">
103 <span className="mr-2 text-faint select-none">$</span>
104 {line}
105 </span>
106 ))
107 : agent.code}
108 </code>
109 </pre>
110 <Button
111 type="button"
112 aria-label={copied ? "Copied" : `Copy the ${agent.label} ${agent.file ? "config" : agent.lang === "text" ? "URL" : "command"}`}
113 onClick={copy}
114 variant="ghost"
115 size="inline"
116 className="absolute top-1.5 right-1.5 p-1.5 text-faint"
117 >
118 {copied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
119 </Button>
120 </div>
121 </Card>
122 {hint && (
123 <p className="mt-2 text-xs leading-5 text-muted">
124 {agent.then.split("`").map((part, index) =>
125 index % 2 ? (
126 <code key={index} className="font-mono text-fg-soft">
127 {part}
128 </code>
129 ) : (
130 part
131 ),
132 )}
133 {chosen === "codex" && mcp === "https://mcp.g1t.sh" && (
134 <> {" "}<a href="https://docs.g1t.sh/guides/bring-your-own-agent/#codex-marketplace" className="text-fg underline underline-offset-4">Install the Codex plugin</a> for issue and session workflows.</>
135 )}
136 {chosen === "cursor" && (
137 <>
138 {" "}
139 <a href={cursorInstallLink(mcp)} className="inline-flex items-center gap-1 text-fg underline underline-offset-4">
140 Add to Cursor <ExternalLink size={11} />
141 </a>
142 </>
143 )}
144 </p>
145 )}
146 </div>
147 );
148}