flagon-io/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

g1t/packages/contracts/src/search.ts

161 lines5,119 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.

Search across all of g1t, Explore, and a command palette1import type { ServiceBinding } from "./clients";
2import type { Viewer } from "./identity";
3import type { Result } from "./result";
4
5/**
6 * Search across all of g1t: repositories (name, description, topics,
7 * README), code on default branches, issues and pull requests, people and
8 * workspaces. Everything public, and private content the viewer is a member
9 * of, decided when the query runs. Mirrors `crates/contracts/src/search.rs`.
10 */
11
12/** One tab of results. */
13export type SearchType = "repositories" | "code" | "issues" | "pulls" | "people";
14
15export const SEARCH_TYPES: SearchType[] = ["repositories", "code", "issues", "pulls", "people"];
16
17/** The most results on one page, and how many a page has when none is asked for. */
18export const MAX_PER_PAGE = 50;
19export const DEFAULT_PER_PAGE = 20;
20/** Counts stop here: past it a tab says "1,000+". */
21export const COUNT_CAP = 1000;
22
23/** A piece of a snippet, highlighted where it matched the query. */
24export type Segment = { text: string; highlight: boolean };
25
26/** A line of code in a result, numbered from 1. */
27export type CodeLine = { number: number; parts: Segment[] };
28
29export type HitKind = "repository" | "code" | "issue" | "pull" | "user" | "workspace";
30
31/** One result. Which fields are set depends on `kind`. */
32export type SiteHit = {
33 kind: HitKind;
34 /** `acme/web`, a file's path, an issue's title, a person's name. */
35 title: string;
36 /** A path on g1t.sh: `/acme/web/blob/main/src/app.rs#L12`. */
37 url: string;
38 /** `owner/name`, for everything but people. */
39 repo: string | null;
40 private: boolean;
41 description: string | null;
42 snippet: Segment[];
43 /** Code only: the lines that matched, with a line around each. */
44 lines: CodeLine[];
45 path: string | null;
46 language: string | null;
47 ref: string | null;
48 number: number | null;
49 /** `open`/`closed` for an issue; `draft`/`open`/`merged`/`closed` for a pull request. */
50 state: string | null;
51 author: string | null;
52 labels: string[];
53 topics: string[];
54 /** A username or a workspace's slug. */
55 slug: string | null;
56 avatar: string | null;
57 /** RFC 3339. */
58 updatedAt: string | null;
59};
60
61export type SearchCounts = Record<SearchType, number>;
62
63export type SearchResults = {
64 /** The query as it was read. */
65 query: string;
66 type: SearchType;
67 counts: SearchCounts;
68 page: number;
69 perPage: number;
70 more: boolean;
71 hits: SiteHit[];
72 /** What the query could not do, said plainly. */
73 notes: string[];
74};
75
76export type ExploreRepo = {
77 namespace: string;
78 name: string;
79 description: string | null;
80 topics: string[];
81 language: string | null;
82 createdAt: string;
83 pushedAt: string | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look84 /** Whether it is archived: read-only, kept for reference. */
85 archived: boolean;
Search across all of g1t, Explore, and a command palette86};
87
88export type Facet = { name: string; count: number };
89
90export type Explore = {
91 repos: ExploreRepo[];
92 languages: Facet[];
93 topics: Facet[];
94 page: number;
95 more: boolean;
96};
97
98export interface SearchApi {
99 /** One page of results of one type, with counts for every type. */
100 search(
101 viewer: Viewer,
102 query: string,
103 options?: { type?: SearchType | null; page?: number; perPage?: number },
104 ): Promise<Result<SearchResults>>;
105 /** A few repositories, issues, pull requests and people, as someone types. */
106 suggest(viewer: Viewer, query: string): Promise<SiteHit[]>;
107 /** Public repositories, recently active or new, by language or topic. */
108 explore(
109 viewer: Viewer,
110 options?: { sort?: "active" | "new"; language?: string | null; topic?: string | null; page?: number },
111 ): Promise<Explore>;
112}
113
114export function searchClient(service: ServiceBinding): SearchApi {
115 const call = async <T>(method: string, args: object): Promise<T> => {
116 const response = await service.fetch(`https://service/rpc/${method}`, {
117 method: "POST",
118 headers: { "content-type": "application/json" },
119 body: JSON.stringify(args),
120 });
121 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
122 return (await response.json()) as T;
123 };
124 return {
125 search: (viewer, query, options = {}) =>
126 call("search", { viewer, query, type: options.type ?? null, page: options.page ?? null, perPage: options.perPage ?? null }),
127 suggest: (viewer, query) => call("suggest", { viewer, query }),
128 explore: (viewer, options = {}) =>
129 call("explore", {
130 viewer,
131 sort: options.sort ?? null,
132 language: options.language ?? null,
133 topic: options.topic ?? null,
134 page: options.page ?? null,
135 }),
136 };
137}
138
139/** Reads a type as an address or a person writes it; null when it is not one. */
140export function searchType(value: string | null | undefined): SearchType | null {
141 switch ((value ?? "").trim().toLowerCase()) {
142 case "repositories":
143 case "repos":
144 case "repo":
145 return "repositories";
146 case "code":
147 return "code";
148 case "issues":
149 case "issue":
150 return "issues";
151 case "pulls":
152 case "prs":
153 case "pr":
154 return "pulls";
155 case "people":
156 case "users":
157 return "people";
158 default:
159 return null;
160 }
161}