Skip to content

g1t/apps/web/app/lib/mention.ts

47 lines2,157 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1/** How g1t's agent is mentioned. The same as `AGENT_HANDLE` in the contracts. */
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent2export const AGENT_MENTION = "@g1t";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API3
4const NAME = AGENT_MENTION.slice(1);
5
6/**
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent7 * The `@…` being typed just before the caret, if it could become `@g1t`:
8 * where it starts. Null otherwise, and once it is `@g1t` already.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API9 */
10export function partialMention(text: string, caret: number): number | null {
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent11 const match = /(?:^|[\s(])@([\w./-]{0,9})$/.exec(text.slice(0, caret));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API12 if (!match) return null;
13 const typed = match[1].toLowerCase();
14 if (typed === NAME || !NAME.startsWith(typed)) return null;
15 return caret - typed.length - 1;
16}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar17
18/** The most suggestions shown at once. */
19export const MAX_SUGGESTIONS = 5;
20
21/** What could complete the `@…` before the caret: where it starts, and the handles that fit, best first. */
22export type MentionMatch = { start: number; options: string[] };
23
24/**
25 * The `@…` being typed just before the caret and what could complete it:
26 * `@g1t` and the `handles` given (such as `@acme/backend`) whose name, or
27 * a team's own part after the `/`, starts with what is typed. At most
28 * `MAX_SUGGESTIONS`; nothing once one is typed in full. Null when the
29 * caret is not in a mention or nothing fits.
30 */
31export function mentionSuggestions(text: string, caret: number, handles: readonly string[] = []): MentionMatch | null {
32 const match = /(?:^|[\s(])@([\w./-]{0,100})$/.exec(text.slice(0, caret));
33 if (!match) return null;
34 const typed = match[1].toLowerCase();
35 const start = caret - match[1].length - 1;
36 const names = [...new Set([AGENT_MENTION, ...handles].map((handle) => (handle.startsWith("@") ? handle : `@${handle}`)))];
37 if (names.some((name) => name.slice(1).toLowerCase() === typed)) return null;
38 const whole: string[] = [];
39 const part: string[] = [];
40 for (const name of names) {
41 const bare = name.slice(1).toLowerCase();
42 if (bare.startsWith(typed)) whole.push(name);
43 else if (typed && !typed.includes("/") && bare.split("/")[1]?.startsWith(typed)) part.push(name);
44 }
45 const options = [...whole, ...part].slice(0, MAX_SUGGESTIONS);
46 return options.length > 0 ? { start, options } : null;
47}