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