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/mention-textarea.tsx

74 lines2,281 bytesCodeBlame
1import { Bot } from "lucide-react";
2import { type ComponentProps, type KeyboardEvent, useRef, useState } from "react";
3
4import { AGENT_MENTION as AGENT_HANDLE, partialMention } from "../lib/mention";
5import { Textarea } from "./ui";
6
7/**
8 * A comment box that offers to complete `@g1t-agent`: Tab or Enter takes
9 * the suggestion, Escape dismisses it.
10 */
11export function MentionTextarea(props: ComponentProps<"textarea">) {
12 const ref = useRef<HTMLTextAreaElement>(null);
13 const [start, setStart] = useState<number | null>(null);
14
15 const look = () => {
16 const box = ref.current;
17 if (!box) return;
18 setStart(box.selectionStart === box.selectionEnd ? partialMention(box.value, box.selectionStart) : null);
19 };
20
21 const complete = () => {
22 const box = ref.current;
23 if (!box || start == null) return;
24 const caret = box.selectionStart;
25 box.setRangeText(`${AGENT_HANDLE} `, start, caret, "end");
26 setStart(null);
27 box.focus();
28 };
29
30 const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
31 if (start != null && (event.key === "Tab" || event.key === "Enter") && !event.shiftKey) {
32 event.preventDefault();
33 complete();
34 return;
35 }
36 if (event.key === "Escape") setStart(null);
37 props.onKeyDown?.(event);
38 };
39
40 return (
41 <div className="relative">
42 <Textarea
43 {...props}
44 ref={ref}
45 onKeyDown={onKeyDown}
46 onInput={(event) => {
47 look();
48 props.onInput?.(event);
49 }}
50 onClick={(event) => {
51 look();
52 props.onClick?.(event);
53 }}
54 onBlur={(event) => {
55 // After a click on the suggestion has landed.
56 setTimeout(() => setStart(null), 150);
57 props.onBlur?.(event);
58 }}
59 />
60 {start != null && (
61 <button
62 type="button"
63 onMouseDown={(event) => event.preventDefault()}
64 onClick={complete}
65 className="absolute bottom-2 left-2 flex items-center gap-1.5 rounded-md border border-line bg-raised px-2 py-1 font-mono text-xs text-fg shadow-sm"
66 >
67 <Bot size={12} className="text-merged" />
68 {AGENT_HANDLE}
69 <span className="font-sans text-faint">Tab</span>
70 </button>
71 )}
72 </div>
73 );
74}