flagon-io/g1t

public

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

g1t/apps/web/app/components/agent-setup.tsx

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