Search across all of g1t, Explore, and a command palette
- services/search: one D1 FTS5 index of repositories (with topics and README), code on default branches (trigram), issues and pull requests, and people and workspaces. Fed by events: a push re-indexes only the files it changed, in capped jobs; a backfill starts on its own. - Visibility is checked when the query runs, against current membership and again against the repos service, so a project made private leaves others' results at once. Signed-out visitors search everything public. - Qualifiers: repo:, workspace:, language:, path:, is:, author:, label:, and quoted phrases. GET /search and the MCP search tool, for people and agents. - /search, a search box in the top bar, a Ctrl-K palette with instant results, per-project search from Code, and Explore for public projects by activity, language and topic. Repositories gain topics. - A workflow can be run by its id as well as by its file.
| 1025 | 1025 | ] | |
| 1026 | 1026 | ||
| 1027 | 1027 | [[package]] | |
| 1028 | + | name = "g1t-search" | |
| 1029 | + | version = "0.1.0" | |
| 1030 | + | dependencies = [ | |
| 1031 | + | "g1t-contracts", | |
| 1032 | + | "g1t-kit", | |
| 1033 | + | "serde", | |
| 1034 | + | "serde_json", | |
| 1035 | + | "worker", | |
| 1036 | + | ] | |
| 1037 | + | ||
| 1038 | + | [[package]] | |
| 1028 | 1039 | name = "g1t-secrets" | |
| 1029 | 1040 | version = "0.1.0" | |
| 1030 | 1041 | dependencies = [ |
| 1 | 1 | [workspace] | |
| 2 | 2 | resolver = "3" | |
| 3 | − | members = ["apps/api", "crates/*", "services/actions", "services/billing", "services/events", "services/identity", "services/integrations", "services/webhooks", "services/repos", "services/security", "services/work"] | |
| 3 | + | members = ["apps/api", "crates/*", "services/actions", "services/billing", "services/events", "services/identity", "services/integrations", "services/webhooks", "services/repos", "services/search", "services/security", "services/work"] | |
| 4 | 4 | ||
| 5 | 5 | [workspace.package] | |
| 6 | 6 | edition = "2024" |
| 122 | 122 | "current_user_url": format!("{API}/user"), | |
| 123 | 123 | "workspaces_url": format!("{API}/workspaces"), | |
| 124 | 124 | "repositories_url": format!("{API}/repos{{?q}}"), | |
| 125 | + | "search_url": format!("{API}/search{{?q,type,page,per_page}}"), | |
| 125 | 126 | "repository_url": repo, | |
| 126 | 127 | "repository_events_url": format!("{repo}/events{{?before}}"), | |
| 127 | 128 | "labels_url": format!("{repo}/labels"), |
| 84 | 84 | &[Op::Remember, Op::Recall], | |
| 85 | 85 | ), | |
| 86 | 86 | ( | |
| 87 | + | "Search", | |
| 88 | + | "One search across all of g1t: repositories, code, issues, pull requests, people and workspaces. Public content for everyone, and private content in workspaces you belong to.", | |
| 89 | + | &[Op::Search], | |
| 90 | + | ), | |
| 91 | + | ( | |
| 87 | 92 | "Context", | |
| 88 | 93 | "A workspace's context hub: a catalog of what it builds and runs, built from its repositories, deployments and integrations, and one search across the catalog, docs, issues, pull requests and memory.", | |
| 89 | 94 | &[Op::SearchContext, Op::GetEntity], | |
| 170 | 175 | Op::Recall => "Recall memory", | |
| 171 | 176 | Op::SearchContext => "Search the context hub", | |
| 172 | 177 | Op::GetEntity => "Get a catalog entry", | |
| 178 | + | Op::Search => "Search g1t", | |
| 173 | 179 | Op::ListIssues => "List issues", | |
| 174 | 180 | Op::GetIssue => "Get an issue", | |
| 175 | 181 | Op::CreateIssue => "Create an issue", | |
| 383 | 389 | ); | |
| 384 | 390 | } | |
| 385 | 391 | responses.insert("403".into(), error_response("Signed in, but not allowed to do this.")); | |
| 386 | − | if !matches!(op, Op::Whoami | Op::ListRepos) { | |
| 392 | + | if !matches!(op, Op::Whoami | Op::ListRepos | Op::Search) { | |
| 387 | 393 | responses.insert("404".into(), error_response("It does not exist, or you cannot see it.")); | |
| 388 | 394 | } | |
| 389 | 395 | if route.method != "GET" { |
| 28 | 28 | pub actions: Fetcher, | |
| 29 | 29 | /// The context hub: catalog and search. | |
| 30 | 30 | pub context: Fetcher, | |
| 31 | + | /// Search across all of g1t. | |
| 32 | + | pub search: Fetcher, | |
| 31 | 33 | /// Where the request came in, for its audit entries. | |
| 32 | 34 | pub audit: crate::audit::AuditContext, | |
| 33 | 35 | /// Set for a request made with an agent's token: all it may do. | |
| 47 | 49 | webhooks: env.service("WEBHOOKS")?, | |
| 48 | 50 | actions: env.service("ACTIONS")?, | |
| 49 | 51 | context: env.service("CONTEXT")?, | |
| 52 | + | search: env.service("SEARCH")?, | |
| 50 | 53 | scope: None, | |
| 51 | 54 | audit: crate::audit::AuditContext::default(), | |
| 52 | 55 | }) | |
| 71 | 74 | Recall, | |
| 72 | 75 | SearchContext, | |
| 73 | 76 | GetEntity, | |
| 77 | + | Search, | |
| 74 | 78 | ListIssues, | |
| 75 | 79 | GetIssue, | |
| 76 | 80 | CreateIssue, | |
| 295 | 299 | } | |
| 296 | 300 | ||
| 297 | 301 | impl Op { | |
| 298 | − | pub const ALL: [Op; 68] = [ | |
| 302 | + | pub const ALL: [Op; 69] = [ | |
| 299 | 303 | Op::Whoami, | |
| 300 | 304 | Op::CreateWorkspace, | |
| 301 | 305 | Op::ListRepos, | |
| 312 | 316 | Op::Recall, | |
| 313 | 317 | Op::SearchContext, | |
| 314 | 318 | Op::GetEntity, | |
| 319 | + | Op::Search, | |
| 315 | 320 | Op::ListIssues, | |
| 316 | 321 | Op::GetIssue, | |
| 317 | 322 | Op::CreateIssue, | |
| 388 | 393 | Op::Recall => "recall", | |
| 389 | 394 | Op::SearchContext => "search_context", | |
| 390 | 395 | Op::GetEntity => "get_entity", | |
| 396 | + | Op::Search => "search", | |
| 391 | 397 | Op::UpdateRepoSettings => "update_repo_settings", | |
| 392 | 398 | Op::ListIssues => "list_issues", | |
| 393 | 399 | Op::GetIssue => "get_issue", | |
| 455 | 461 | Op::ListRepos => "Repositories you can see, optionally filtered by a search query.", | |
| 456 | 462 | Op::GetRepo => "One repository's details.", | |
| 457 | 463 | Op::UpdateRepo => { | |
| 458 | − | "Change a repository's description, whether it is private, and whether its default branch is protected. A protected branch refuses pushes and changes only by merging a pull request. Only the fields given are changed. Members of its workspace only." | |
| 464 | + | "Change a repository's description, its topics, whether it is private, and whether its default branch is protected. A protected branch refuses pushes and changes only by merging a pull request. Only the fields given are changed. Members of its workspace only." | |
| 459 | 465 | } | |
| 460 | 466 | Op::GetRepoSettings => { | |
| 461 | 467 | "How a repository handles pull requests: the approvals a merge needs, whether failed checks can be overridden, whether a pull request must be up to date, and how g1t's agents are reviewed, revised and merged." | |
| 478 | 484 | Op::SearchContext => { | |
| 479 | 485 | "One search across a workspace's context hub: its catalog (projects, apps, APIs, packages, languages, owners, environments, integrations, docs), the text of its docs, its issues and pull requests, and, for members and g1t's agents, its kept memory. Results are ranked by meaning, each labelled with its kind, where it came from, who wrote it and how fresh it is; matching words answers when meaning cannot. Give the workspace, or a repository in it. Narrow with project (a project's slug) and kinds. Reads only what you may see: memory and private projects are for members." | |
| 480 | 486 | } | |
| 487 | + | Op::Search => { | |
| 488 | + | "Search all of g1t: repositories (name, description, topics, README), code on default branches (file names and contents), issues, pull requests, people and workspaces. Covers everything public, and private content in workspaces you belong to; signed out, public only. Write words, \"exact phrases\", -words to leave out, and qualifiers: repo:owner/name, org:workspace, language:rust, path:src/ (a glob with *), is:issue, is:pr, is:open, is:closed, is:merged, author:username, label:bug. type picks the kind of results (repositories, code, issues, pulls or people); without it, the qualifiers decide. Returns one page of results with the matches highlighted, code with line numbers, and how many there are of each kind." | |
| 489 | + | } | |
| 481 | 490 | Op::GetEntity => { | |
| 482 | 491 | "One entry of a workspace's catalog, by kind and its id or key (a project's slug, a package as npm:<name>, an owner's username), with every relation it has: what it depends on, who owns it, where it deploys, what documents it, what it exposes and uses. search_context finds entries." | |
| 483 | 492 | } | |
| 656 | 665 | "type": "boolean", | |
| 657 | 666 | "description": "Refuse pushes to the default branch, so that it changes only by merging a pull request.", | |
| 658 | 667 | }, | |
| 668 | + | "topics": { | |
| 669 | + | "type": "array", | |
| 670 | + | "items": { "type": "string" }, | |
| 671 | + | "description": "Replaces its topics, which search and Explore show: lowercase letters, digits and hyphens, at most 20. An empty list clears them.", | |
| 672 | + | }, | |
| 659 | 673 | }), | |
| 660 | 674 | &["repo"], | |
| 661 | 675 | ), | |
| 733 | 747 | }), | |
| 734 | 748 | &["query"], | |
| 735 | 749 | ), | |
| 750 | + | Op::Search => object( | |
| 751 | + | json!({ | |
| 752 | + | "query": { "type": "string", "description": "What to look for: words, \"phrases\" and qualifiers, such as parse_query language:rust repo:acme/web." }, | |
| 753 | + | "type": { | |
| 754 | + | "type": "string", | |
| 755 | + | "enum": ["repositories", "code", "issues", "pulls", "people"], | |
| 756 | + | "description": "Which kind of results. Worked out from the qualifiers if not given: path: means code, is:pr pull requests, is:open or label: issues, otherwise repositories.", | |
| 757 | + | }, | |
| 758 | + | "page": { "type": "integer", "description": "From 1; at most 50." }, | |
| 759 | + | "per_page": { "type": "integer", "description": "At most 50; 20 if not given." }, | |
| 760 | + | }), | |
| 761 | + | &["query"], | |
| 762 | + | ), | |
| 736 | 763 | Op::GetEntity => object( | |
| 737 | 764 | json!({ | |
| 738 | 765 | "kind": { | |
| 1184 | 1211 | !matches!( | |
| 1185 | 1212 | self, | |
| 1186 | 1213 | Op::ListRepos | |
| 1214 | + | | Op::Search | |
| 1187 | 1215 | | Op::GetRepo | |
| 1188 | 1216 | | Op::ListIssues | |
| 1189 | 1217 | | Op::GetIssue | |
| 1211 | 1239 | | Op::CreateWorkspace | |
| 1212 | 1240 | | Op::SearchContext | |
| 1213 | 1241 | | Op::GetEntity | |
| 1242 | + | | Op::Search | |
| 1214 | 1243 | | Op::ListRepos | |
| 1215 | 1244 | | Op::CreateRepo | |
| 1216 | 1245 | | Op::ListIntegrations | |
| 1382 | 1411 | "description": input["description"].as_str(), | |
| 1383 | 1412 | "isPrivate": input["private"].as_bool(), | |
| 1384 | 1413 | "protected": input["protected"].as_bool(), | |
| 1414 | + | "topics": strings(input, "topics"), | |
| 1385 | 1415 | }), | |
| 1386 | 1416 | ) | |
| 1387 | 1417 | .await | |
| 1495 | 1525 | .await | |
| 1496 | 1526 | } | |
| 1497 | 1527 | } | |
| 1528 | + | Op::Search => { | |
| 1529 | + | pass( | |
| 1530 | + | &services.search, | |
| 1531 | + | "search", | |
| 1532 | + | &json!({ | |
| 1533 | + | "viewer": viewer, | |
| 1534 | + | "query": text(input, "query"), | |
| 1535 | + | "type": optional_text(input, "type").and_then(|kind| { | |
| 1536 | + | g1t_contracts::search::SearchType::parse(&kind).map(|kind| kind.as_str()) | |
| 1537 | + | }), | |
| 1538 | + | "page": integer(input, "page"), | |
| 1539 | + | "perPage": integer(input, "per_page"), | |
| 1540 | + | }), | |
| 1541 | + | ) | |
| 1542 | + | .await | |
| 1543 | + | } | |
| 1498 | 1544 | Op::Recall => { | |
| 1499 | 1545 | pass( | |
| 1500 | 1546 | work, |
| 2625 | 2625 | }, | |
| 2626 | 2626 | "notes": "Members of the workspace only. Each memory returned is marked used, which keeps it near the front of what agents are given." | |
| 2627 | 2627 | }, | |
| 2628 | + | "search": { | |
| 2629 | + | "query": { | |
| 2630 | + | "q": "parse_query language:rust repo:acme/web", | |
| 2631 | + | "type": "code" | |
| 2632 | + | }, | |
| 2633 | + | "response": { | |
| 2634 | + | "query": "parse_query repo:acme/web language:rust", | |
| 2635 | + | "type": "code", | |
| 2636 | + | "counts": { | |
| 2637 | + | "repositories": 0, | |
| 2638 | + | "code": 2, | |
| 2639 | + | "issues": 0, | |
| 2640 | + | "pulls": 0, | |
| 2641 | + | "people": 0 | |
| 2642 | + | }, | |
| 2643 | + | "page": 1, | |
| 2644 | + | "per_page": 20, | |
| 2645 | + | "more": false, | |
| 2646 | + | "hits": [ | |
| 2647 | + | { | |
| 2648 | + | "kind": "code", | |
| 2649 | + | "title": "src/search/query.rs", | |
| 2650 | + | "url": "/acme/web/blob/main/src/search/query.rs#L42", | |
| 2651 | + | "repo": "acme/web", | |
| 2652 | + | "private": false, | |
| 2653 | + | "description": null, | |
| 2654 | + | "snippet": [], | |
| 2655 | + | "lines": [ | |
| 2656 | + | { | |
| 2657 | + | "number": 41, | |
| 2658 | + | "parts": [ | |
| 2659 | + | { | |
| 2660 | + | "text": "/// Reads a query typed into the search box.", | |
| 2661 | + | "highlight": false | |
| 2662 | + | } | |
| 2663 | + | ] | |
| 2664 | + | }, | |
| 2665 | + | { | |
| 2666 | + | "number": 42, | |
| 2667 | + | "parts": [ | |
| 2668 | + | { | |
| 2669 | + | "text": "pub fn ", | |
| 2670 | + | "highlight": false | |
| 2671 | + | }, | |
| 2672 | + | { | |
| 2673 | + | "text": "parse_query", | |
| 2674 | + | "highlight": true | |
| 2675 | + | }, | |
| 2676 | + | { | |
| 2677 | + | "text": "(text: &str) -> Query {", | |
| 2678 | + | "highlight": false | |
| 2679 | + | } | |
| 2680 | + | ] | |
| 2681 | + | }, | |
| 2682 | + | { | |
| 2683 | + | "number": 43, | |
| 2684 | + | "parts": [ | |
| 2685 | + | { | |
| 2686 | + | "text": " let mut query = Query::default();", | |
| 2687 | + | "highlight": false | |
| 2688 | + | } | |
| 2689 | + | ] | |
| 2690 | + | } | |
| 2691 | + | ], | |
| 2692 | + | "path": "src/search/query.rs", | |
| 2693 | + | "language": "rust", | |
| 2694 | + | "ref": "main", | |
| 2695 | + | "number": null, | |
| 2696 | + | "state": null, | |
| 2697 | + | "author": null, | |
| 2698 | + | "labels": [], | |
| 2699 | + | "topics": [], | |
| 2700 | + | "slug": null, | |
| 2701 | + | "avatar": null, | |
| 2702 | + | "updated_at": null | |
| 2703 | + | } | |
| 2704 | + | ], | |
| 2705 | + | "notes": [] | |
| 2706 | + | }, | |
| 2707 | + | "notes": "`q` takes words, `\"exact phrases\"`, `-words` to leave out, and qualifiers: `repo:owner/name`, `org:` (or `workspace:`), `language:`, `path:` (a glob when it has `*`), `is:issue`, `is:pr`, `is:open`, `is:closed`, `is:merged`, `is:draft`, `is:public`, `is:private`, `author:` and `label:`; most can be left out with a leading `-`, as in `-label:wontfix`. `type` is `repositories`, `code`, `issues`, `pulls` or `people`; without it, `path:` means code, `is:pr` pull requests, `is:open`, `author:` or `label:` issues, and anything else repositories. `counts` says how many results each type has, up to 1,000. Each result's `snippet` (or, for code, each of its `lines`) is a list of parts, `highlight` true where the query matched. Code is searched on default branches and needs a word of three characters or more, unless the query names a `repo:`. Public content is returned to anyone, without a token; private content only to members of its workspace, checked when the search runs, so a repository made private stops appearing at once. A g1t agent's token can search too." | |
| 2708 | + | }, | |
| 2628 | 2709 | "search_context": { | |
| 2629 | 2710 | "params": { | |
| 2630 | 2711 | "workspace": "acme" |
| 7 | 7 | //! encoded again, so that every field the type has is sent, not only the | |
| 8 | 8 | //! ones an example shows. | |
| 9 | 9 | ||
| 10 | − | use g1t_contracts::{actions, integrations, repos, webhooks, work}; | |
| 10 | + | use g1t_contracts::{actions, integrations, repos, search, webhooks, work}; | |
| 11 | 11 | use g1t_kit::wire::{self, USER_KEYED}; | |
| 12 | 12 | use serde::Serialize; | |
| 13 | 13 | use serde::de::DeserializeOwned; | |
| 66 | 66 | let sent = as_services_send(example); | |
| 67 | 67 | match op { | |
| 68 | 68 | Op::ListRepos => through::<Vec<repos::Repo>>(op, sent), | |
| 69 | + | Op::Search => through::<search::SearchResults>(op, sent), | |
| 69 | 70 | Op::GetRepo | Op::CreateRepo | Op::UpdateRepo => through::<repos::Repo>(op, sent), | |
| 70 | 71 | Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent), | |
| 71 | 72 | Op::GetMergeQueue => through::<work::QueueView>(op, sent), |
| 31 | 31 | route("GET", "/user", Op::Whoami, &[]), | |
| 32 | 32 | route("POST", "/workspaces", Op::CreateWorkspace, &[]), | |
| 33 | 33 | route("GET", "/repos", Op::ListRepos, &[("q", "query")]), | |
| 34 | + | route( | |
| 35 | + | "GET", | |
| 36 | + | "/search", | |
| 37 | + | Op::Search, | |
| 38 | + | &[("q", "query"), ("type", "type"), ("page", "page"), ("per_page", "per_page")], | |
| 39 | + | ), | |
| 34 | 40 | route("POST", "/repos", Op::CreateRepo, &[]), | |
| 35 | 41 | route("GET", "/repos/:owner/:name", Op::GetRepo, &[]), | |
| 36 | 42 | route("PATCH", "/repos/:owner/:name", Op::UpdateRepo, &[]), |
| 25 | 25 | // Builds of deployments report through here. | |
| 26 | 26 | { "binding": "DEPLOYMENTS", "service": "g1t-deployments" }, | |
| 27 | 27 | // The context hub: search_context and get_entity. | |
| 28 | − | { "binding": "CONTEXT", "service": "g1t-context" } | |
| 28 | + | { "binding": "CONTEXT", "service": "g1t-context" }, | |
| 29 | + | { "binding": "SEARCH", "service": "g1t-search" } | |
| 29 | 30 | ], | |
| 30 | 31 | // GitHub Actions artifacts and cache, in chunks, with KV's own expiry. | |
| 31 | 32 | // (Moves to R2 once R2 is enabled on the account.) |
| 73 | 73 | items: [ | |
| 74 | 74 | { label: 'Quickstart', slug: 'quickstart' }, | |
| 75 | 75 | { label: 'How g1t works', slug: 'concepts/overview' }, | |
| 76 | + | { label: 'Search and Explore', slug: 'guides/search' }, | |
| 76 | 77 | ], | |
| 77 | 78 | }, | |
| 78 | 79 | { |
| 367 | 367 | ||
| 368 | 368 | | Kind of run | Git | API and MCP tools | | |
| 369 | 369 | | --- | --- | --- | | |
| 370 | − | | Implement | Reads the repository; pushes to its pull request's fork only | Records the session and marks its own pull request ready; tools to read issues, pull requests, the merge queue, workflow runs and memory, open issues, comment, remember, and message other agents | | |
| 370 | + | | Implement | Reads the repository; pushes to its pull request's fork only | Records the session and marks its own pull request ready; tools to read issues, pull requests, the merge queue, workflow runs and memory, to [search all of g1t](/guides/search/) and the workspace's context hub, open issues, comment, remember, and message other agents | | |
| 371 | 371 | | Revise, answer | Reads the repository; pushes to the pull request's fork, or to its branch only when the change is a branch of the repository | Records the session of its own pull request; the same tools as implement | | |
| 372 | 372 | | Catch up | Reads the repository; pushes to the pull request's fork or branch only | Records the session of its own pull request | | |
| 373 | 373 | | Review | Reads the change and the repository; pushes nothing | Reports its review through its own run | |
| 52 | 52 | | **Deployments** | Production, previews, build command, output directory and idle days. See [Deployments](/guides/deployments/#settings). | | |
| 53 | 53 | | **Dependencies** | The projects this one uses, and the ones that use it. See [Dependencies](#dependencies). | | |
| 54 | 54 | | **Secrets and variables** | The project's rows. See [Secrets and variables](/guides/secrets-and-variables/). | | |
| 55 | − | | **Repository** | The repository's visibility, branch protection, required approvals, checks, the merge queue and auto-merge. | | |
| 55 | + | | **Repository** | The repository's description, [topics](/guides/search/#what-is-indexed), visibility, branch protection, required approvals, checks, the merge queue and auto-merge. | | |
| 56 | 56 | | **Webhooks** | The repository's [webhooks](/guides/webhooks/). | | |
| 57 | 57 | ||
| 58 | 58 | The **root directory** says where in the repository the project lives, |
| 1 | + | --- | |
| 2 | + | title: Search and Explore | |
| 3 | + | description: Search all of g1t at once (repositories, code, issues, pull requests, people and workspaces) with qualifiers, and browse public projects on Explore. | |
| 4 | + | --- | |
| 5 | + | ||
| 6 | + | **Search** looks through all of g1t at once: repositories, the code on | |
| 7 | + | their default branches, issues, pull requests, people and workspaces. It | |
| 8 | + | covers everything public, and everything private in the workspaces you | |
| 9 | + | belong to. Signed out, you see public results only. | |
| 10 | + | ||
| 11 | + | **Explore**, at [g1t.sh/explore](https://g1t.sh/explore), lists public | |
| 12 | + | projects: the recently active ones, the new ones, and the ones in a | |
| 13 | + | language or about a topic. | |
| 14 | + | ||
| 15 | + | ## Search from anywhere | |
| 16 | + | ||
| 17 | + | There are three ways in: | |
| 18 | + | ||
| 19 | + | | Where | What it does | | |
| 20 | + | | --- | --- | | |
| 21 | + | | The search box in the top bar | Opens [g1t.sh/search](https://g1t.sh/search) with what you typed | | |
| 22 | + | | **⌘K** (Ctrl-K on Windows and Linux) | Opens the command palette. As you type it shows matching repositories, issues, pull requests and people, the pages you can go to, and rows that search all of g1t or only code for what you typed | | |
| 23 | + | | A project's **Code** page | **Search this repository's code** searches the project's repository, with `repo:` filled in | | |
| 24 | + | ||
| 25 | + | The search page has a tab for each kind of result, each with its count: | |
| 26 | + | ||
| 27 | + | | Tab | Searches | | |
| 28 | + | | --- | --- | | |
| 29 | + | | **Repositories** | Each repository's name, workspace, description, topics and the opening of its README | | |
| 30 | + | | **Code** | The file names and contents of every repository's default branch | | |
| 31 | + | | **Issues** | Issue titles and descriptions | | |
| 32 | + | | **Pull requests** | Pull request titles and descriptions | | |
| 33 | + | | **People** | Usernames and names of people, and the slugs, names and descriptions of workspaces | | |
| 34 | + | ||
| 35 | + | Results show the part that matched, highlighted. A code result shows the | |
| 36 | + | lines that matched with their line numbers, and each line links to that | |
| 37 | + | line of the file, at `/<owner>/<repo>/blob/<branch>/<path>#L<line>`. Counts | |
| 38 | + | stop at 1,000. Results come 20 to a page. | |
| 39 | + | ||
| 40 | + | ## Writing a query | |
| 41 | + | ||
| 42 | + | Words match wherever they appear, in any order. A result has every word | |
| 43 | + | you type. | |
| 44 | + | ||
| 45 | + | | You type | It finds | | |
| 46 | + | | --- | --- | | |
| 47 | + | | `merge queue` | Results with both words, anywhere | | |
| 48 | + | | `"merge queue"` | The two words together, in this order | | |
| 49 | + | | `parse -legacy` | Results with `parse` and without `legacy` | | |
| 50 | + | | `pars` | In repositories, issues, pull requests and people, any word that starts with `pars`, such as `parser` | | |
| 51 | + | | `parse_query(` | In code, that exact run of characters, punctuation and all | | |
| 52 | + | ||
| 53 | + | Code is matched as text, the way you would find something in an editor: | |
| 54 | + | any run of three characters or more, inside words or across them. A code | |
| 55 | + | search needs at least one word of three characters or more, unless it is | |
| 56 | + | limited to repositories with `repo:`. | |
| 57 | + | ||
| 58 | + | ### Qualifiers | |
| 59 | + | ||
| 60 | + | Put qualifiers anywhere in the query. Most can be left out with a leading | |
| 61 | + | `-`, such as `-label:wontfix`. | |
| 62 | + | ||
| 63 | + | | Qualifier | Narrows to | Applies to | | |
| 64 | + | | --- | --- | --- | | |
| 65 | + | | `repo:owner/name` | One repository. Give it more than once for several | Repositories, code, issues, pull requests | | |
| 66 | + | | `org:acme` or `workspace:acme` | One workspace | Repositories, code, issues, pull requests | | |
| 67 | + | | `language:rust` | Code in a language, or repositories mostly written in it. `ts`, `js`, `py` and `cpp` are understood | Repositories, code | | |
| 68 | + | | `path:src/` | Code whose path contains `src/`. With `*`, a pattern: `path:*.rs`, `path:src/*/mod.rs` | Code | | |
| 69 | + | | `is:issue`, `is:pr` | Only issues, or only pull requests | Issues, pull requests | | |
| 70 | + | | `is:open`, `is:closed` | Issues and pull requests by state | Issues, pull requests | | |
| 71 | + | | `is:merged`, `is:draft` | Pull requests that were merged, or are still drafts | Pull requests | | |
| 72 | + | | `is:public`, `is:private` | Results from public or private repositories | Repositories, code, issues, pull requests | | |
| 73 | + | | `author:ana` | Opened by someone | Issues, pull requests | | |
| 74 | + | | `label:bug` | With a label. Quote a label with spaces: `label:"good first issue"` | Issues, pull requests | | |
| 75 | + | | `type:code` | Which tab to open: `repositories`, `code`, `issues`, `pulls` or `people` | All | | |
| 76 | + | ||
| 77 | + | Without a tab or `type:`, the qualifiers choose one: `path:` opens Code, | |
| 78 | + | `is:pr` opens Pull requests, `is:open`, `author:` or `label:` open Issues, | |
| 79 | + | and anything else opens Repositories. A qualifier that only one kind of | |
| 80 | + | result has rules the others out: a query with `path:` finds no people. | |
| 81 | + | ||
| 82 | + | Some examples: | |
| 83 | + | ||
| 84 | + | ```text | |
| 85 | + | fetchEvents language:typescript path:app/ | |
| 86 | + | "rate limit" org:acme is:issue is:open | |
| 87 | + | crash author:ana -label:wontfix | |
| 88 | + | router repo:acme/web repo:acme/api | |
| 89 | + | ``` | |
| 90 | + | ||
| 91 | + | ## What is indexed | |
| 92 | + | ||
| 93 | + | Search is kept current as things change: | |
| 94 | + | ||
| 95 | + | | What | When it is indexed | | |
| 96 | + | | --- | --- | | |
| 97 | + | | A repository's name, description, topics and README | When it is created or its settings change, and on every push to its default branch | | |
| 98 | + | | Code | On every push to the default branch: only the files the push changed | | |
| 99 | + | | Issues and pull requests | When they are opened, edited, assigned, closed, reopened, marked ready or merged | | |
| 100 | + | | People and workspaces | When an account or a workspace is made, or its name, description or avatar changes | | |
| 101 | + | ||
| 102 | + | Code is indexed from default branches only. Some files are left out: | |
| 103 | + | ||
| 104 | + | - vendored and generated directories, such as `node_modules`, `vendor`, | |
| 105 | + | `third_party`, `dist`, `target` and `.next`; | |
| 106 | + | - lockfiles, such as `package-lock.json`, `pnpm-lock.yaml`, `Cargo.lock` | |
| 107 | + | and `go.sum`; | |
| 108 | + | - binary files (images, fonts, archives, compiled code) and minified files; | |
| 109 | + | - files larger than 512 KB. | |
| 110 | + | ||
| 111 | + | A push that changes more than 300 files has the whole default branch | |
| 112 | + | compared instead, and files are read a few dozen at a time in the | |
| 113 | + | background, so very large pushes appear in search over a few minutes. A | |
| 114 | + | repository's first 5,000 files are indexed. | |
| 115 | + | ||
| 116 | + | Set a repository's **topics** in **Settings → Repository**. Topics show on | |
| 117 | + | the project's page and in search results, and each opens Explore for that | |
| 118 | + | topic. | |
| 119 | + | ||
| 120 | + | ## Who sees what | |
| 121 | + | ||
| 122 | + | Search shows you exactly what you could open yourself: | |
| 123 | + | ||
| 124 | + | - Public repositories, and their code, issues and pull requests, are | |
| 125 | + | shown to everyone, signed in or not. | |
| 126 | + | - Private repositories, and their code, issues and pull requests, are shown | |
| 127 | + | only to members of the workspace that owns them, and to that workspace's | |
| 128 | + | agents. | |
| 129 | + | - People and workspaces are public, as their pages are. Search never shows | |
| 130 | + | which workspaces someone belongs to. | |
| 131 | + | ||
| 132 | + | Visibility is checked when you search, against your memberships and each | |
| 133 | + | repository's visibility as they are at that moment, not as they were when | |
| 134 | + | something was indexed. A repository made private disappears from everyone | |
| 135 | + | else's results at once; one made public appears in them within moments. | |
| 136 | + | Someone removed from a workspace stops seeing its private results on their | |
| 137 | + | next search. | |
| 138 | + | ||
| 139 | + | ## Explore | |
| 140 | + | ||
| 141 | + | [Explore](https://g1t.sh/explore) lists public projects only: | |
| 142 | + | ||
| 143 | + | | View | Shows | | |
| 144 | + | | --- | --- | | |
| 145 | + | | **Recently active** | Projects by their last push to the default branch | | |
| 146 | + | | **New** | Projects by when they were created | | |
| 147 | + | | **Languages** | The languages public projects are mostly written in, with how many; choose one to see its projects | | |
| 148 | + | | **Topics** | The topics public projects have, with how many; choose one to see its projects | | |
| 149 | + | ||
| 150 | + | A project's language is the one most of its indexed code is written in, | |
| 151 | + | leaving out prose and data such as Markdown, JSON and YAML. | |
| 152 | + | ||
| 153 | + | ## From the API and agents | |
| 154 | + | ||
| 155 | + | The same search is `GET /search` in the API and the `search` tool over | |
| 156 | + | MCP. It takes `q` (the query), `type` and `page`, and needs no token for | |
| 157 | + | public results: | |
| 158 | + | ||
| 159 | + | ```sh | |
| 160 | + | curl "https://api.g1t.sh/search?q=parse_query+language:rust&type=code" | |
| 161 | + | ``` | |
| 162 | + | ||
| 163 | + | With a token, private results in your workspaces are included. g1t's own | |
| 164 | + | agents can search too, with the token their run is given. See | |
| 165 | + | [Search g1t](/reference/api/search/search/) in the API reference and | |
| 166 | + | [MCP tools](/reference/mcp/#search). | |
| 167 | + | ||
| 168 | + | `search` looks across all of g1t. To ask about one workspace's catalog, | |
| 169 | + | docs and memory, use the [context hub](/guides/context-hub/) and its | |
| 170 | + | `search_context` tool. |
| 38 | 38 | | `list_repos` | | Repositories you can see, optionally filtered by `query`. | [`GET /repos?q=`](/reference/api/repositories/list-repos/) | | |
| 39 | 39 | | `get_repo` | `repo` | One repository's details. | [`GET /repos/{owner}/{name}`](/reference/api/repositories/get-repo/) | | |
| 40 | 40 | | `create_repo` | `name` | Create a repository in one of your workspaces, empty or as a copy of a public git repository (`import_url`). `workspace` may be left out if you belong to exactly one. | [`POST /repos`](/reference/api/repositories/create-repo/) | | |
| 41 | − | | `update_repo` | `repo` | Change its description, whether it is private, and whether its default branch is protected. Members only. | [`PATCH /repos/{owner}/{name}`](/reference/api/repositories/update-repo/) | | |
| 41 | + | | `update_repo` | `repo` | Change its description, its `topics`, whether it is private, and whether its default branch is protected. Members only. | [`PATCH /repos/{owner}/{name}`](/reference/api/repositories/update-repo/) | | |
| 42 | 42 | | `get_repo_settings` | `repo` | How it handles pull requests: approvals, checks, being up to date, and how g1t's agents are reviewed, revised and merged. | [`GET /repos/{owner}/{name}/settings`](/reference/api/repositories/get-repo-settings/) | | |
| 43 | 43 | | `update_repo_settings` | `repo` | Change those settings. Only the fields given change. Members only. | [`PATCH /repos/{owner}/{name}/settings`](/reference/api/repositories/update-repo-settings/) | | |
| 44 | 44 | | `list_labels` | `repo` | The labels available on its issues. | [`GET /repos/{owner}/{name}/labels`](/reference/api/issues/list-labels/) | | |
| 49 | 49 | `max_revisions`, `auto_merge` and `merge_queue`. See | |
| 50 | 50 | [what a repository can ask for](/guides/g1t-agents/#what-a-repository-can-ask-for). | |
| 51 | 51 | ||
| 52 | + | ## Search | |
| 53 | + | ||
| 54 | + | | Tool | Required | What it does | Route | | |
| 55 | + | | --- | --- | --- | --- | | |
| 56 | + | | `search` | `query` | Search all of g1t: repositories, code on default branches, issues, pull requests, people and workspaces. Public results for everyone; private ones in workspaces you belong to. `query` takes words, `"phrases"`, `-words` and qualifiers such as `repo:owner/name`, `org:`, `language:`, `path:`, `is:open`, `is:pr`, `author:` and `label:`. `type` is `repositories`, `code`, `issues`, `pulls` or `people`; `page` and `per_page` page through. Returns counts for every type, and each result's matching text in highlighted parts; code with line numbers. | [`GET /search`](/reference/api/search/search/) | | |
| 57 | + | ||
| 58 | + | See [search and Explore](/guides/search/) for the full syntax. `search` | |
| 59 | + | looks across all of g1t; `search_context`, under [Memory](#memory), asks one | |
| 60 | + | workspace's context hub. | |
| 61 | + | ||
| 52 | 62 | ## Issues | |
| 53 | 63 | ||
| 54 | 64 | | Tool | Required | What it does | Route | | |
| 185 | 195 | ||
| 186 | 196 | | Run | Tools | | |
| 187 | 197 | | --- | --- | | |
| 188 | − | | Implement, revise, answer | `get_repo`, `list_issues`, `get_issue`, `list_labels`, `list_pull_requests`, `get_pull_request`, `get_pull_request_changes`, `read_session`, `get_merge_queue`, `list_events`, `recall`, `search_context`, `get_entity`, `list_workflows`, `list_workflow_runs`, `get_workflow_run`, `get_job_logs`, and `create_issue`, `add_comment`, `take_messages`, `remember`, `message_agent`, `answer_message`, `get_context` | | |
| 198 | + | | Implement, revise, answer | `get_repo`, `list_issues`, `get_issue`, `list_labels`, `list_pull_requests`, `get_pull_request`, `get_pull_request_changes`, `read_session`, `get_merge_queue`, `list_events`, `recall`, `search_context`, `get_entity`, `search`, `list_workflows`, `list_workflow_runs`, `get_workflow_run`, `get_job_logs`, and `create_issue`, `add_comment`, `take_messages`, `remember`, `message_agent`, `answer_message`, `get_context` | | |
| 189 | 199 | | Review | The same reading tools, and `add_comment`, `review_pull_request`, `get_context` | | |
| 190 | 200 | | Plan | The same reading tools, and `create_issue`, `get_context` | | |
| 191 | 201 | | Catch up | The reading tools only | |
| 107 | 107 | ] | |
| 108 | 108 | }, | |
| 109 | 109 | { | |
| 110 | + | "name": "Search", | |
| 111 | + | "description": "One search across all of g1t: repositories, code, issues, pull requests, people and workspaces. Public content for everyone, and private content in workspaces you belong to.", | |
| 112 | + | "x-tools": [ | |
| 113 | + | "search" | |
| 114 | + | ] | |
| 115 | + | }, | |
| 116 | + | { | |
| 110 | 117 | "name": "Context", | |
| 111 | 118 | "description": "A workspace's context hub: a catalog of what it builds and runs, built from its repositories, deployments and integrations, and one search across the catalog, docs, issues, pull requests and memory.", | |
| 112 | 119 | "x-tools": [ | |
| 703 | 710 | } | |
| 704 | 711 | } | |
| 705 | 712 | }, | |
| 713 | + | "/search": { | |
| 714 | + | "get": { | |
| 715 | + | "operationId": "search", | |
| 716 | + | "tags": [ | |
| 717 | + | "Search" | |
| 718 | + | ], | |
| 719 | + | "summary": "Search g1t", | |
| 720 | + | "description": "Search all of g1t: repositories (name, description, topics, README), code on default branches (file names and contents), issues, pull requests, people and workspaces. Covers everything public, and private content in workspaces you belong to; signed out, public only. Write words, \"exact phrases\", -words to leave out, and qualifiers: repo:owner/name, org:workspace, language:rust, path:src/ (a glob with *), is:issue, is:pr, is:open, is:closed, is:merged, author:username, label:bug. type picks the kind of results (repositories, code, issues, pulls or people); without it, the qualifiers decide. Returns one page of results with the matches highlighted, code with line numbers, and how many there are of each kind.\n\n`q` takes words, `\"exact phrases\"`, `-words` to leave out, and qualifiers: `repo:owner/name`, `org:` (or `workspace:`), `language:`, `path:` (a glob when it has `*`), `is:issue`, `is:pr`, `is:open`, `is:closed`, `is:merged`, `is:draft`, `is:public`, `is:private`, `author:` and `label:`; most can be left out with a leading `-`, as in `-label:wontfix`. `type` is `repositories`, `code`, `issues`, `pulls` or `people`; without it, `path:` means code, `is:pr` pull requests, `is:open`, `author:` or `label:` issues, and anything else repositories. `counts` says how many results each type has, up to 1,000. Each result's `snippet` (or, for code, each of its `lines`) is a list of parts, `highlight` true where the query matched. Code is searched on default branches and needs a word of three characters or more, unless the query names a `repo:`. Public content is returned to anyone, without a token; private content only to members of its workspace, checked when the search runs, so a repository made private stops appearing at once. A g1t agent's token can search too.", | |
| 721 | + | "x-mcp-tool": "search", | |
| 722 | + | "security": [ | |
| 723 | + | { | |
| 724 | + | "token": [] | |
| 725 | + | }, | |
| 726 | + | {} | |
| 727 | + | ], | |
| 728 | + | "parameters": [ | |
| 729 | + | { | |
| 730 | + | "name": "q", | |
| 731 | + | "in": "query", | |
| 732 | + | "required": true, | |
| 733 | + | "schema": { | |
| 734 | + | "type": "string" | |
| 735 | + | }, | |
| 736 | + | "description": "What to look for: words, \"phrases\" and qualifiers, such as parse_query language:rust repo:acme/web." | |
| 737 | + | }, | |
| 738 | + | { | |
| 739 | + | "name": "type", | |
| 740 | + | "in": "query", | |
| 741 | + | "required": false, | |
| 742 | + | "schema": { | |
| 743 | + | "type": "string", | |
| 744 | + | "enum": [ | |
| 745 | + | "repositories", | |
| 746 | + | "code", | |
| 747 | + | "issues", | |
| 748 | + | "pulls", | |
| 749 | + | "people" | |
| 750 | + | ] | |
| 751 | + | }, | |
| 752 | + | "description": "Which kind of results. Worked out from the qualifiers if not given: path: means code, is:pr pull requests, is:open or label: issues, otherwise repositories." | |
| 753 | + | }, | |
| 754 | + | { | |
| 755 | + | "name": "page", | |
| 756 | + | "in": "query", | |
| 757 | + | "required": false, | |
| 758 | + | "schema": { | |
| 759 | + | "type": "integer" | |
| 760 | + | }, | |
| 761 | + | "description": "From 1; at most 50." | |
| 762 | + | }, | |
| 763 | + | { | |
| 764 | + | "name": "per_page", | |
| 765 | + | "in": "query", | |
| 766 | + | "required": false, | |
| 767 | + | "schema": { | |
| 768 | + | "type": "integer" | |
| 769 | + | }, | |
| 770 | + | "description": "At most 50; 20 if not given." | |
| 771 | + | } | |
| 772 | + | ], | |
| 773 | + | "responses": { | |
| 774 | + | "200": { | |
| 775 | + | "description": "Success.", | |
| 776 | + | "content": { | |
| 777 | + | "application/json": { | |
| 778 | + | "schema": {}, | |
| 779 | + | "example": { | |
| 780 | + | "query": "parse_query repo:acme/web language:rust", | |
| 781 | + | "type": "code", | |
| 782 | + | "counts": { | |
| 783 | + | "repositories": 0, | |
| 784 | + | "code": 2, | |
| 785 | + | "issues": 0, | |
| 786 | + | "pulls": 0, | |
| 787 | + | "people": 0 | |
| 788 | + | }, | |
| 789 | + | "page": 1, | |
| 790 | + | "per_page": 20, | |
| 791 | + | "more": false, | |
| 792 | + | "hits": [ | |
| 793 | + | { | |
| 794 | + | "kind": "code", | |
| 795 | + | "title": "src/search/query.rs", | |
| 796 | + | "url": "/acme/web/blob/main/src/search/query.rs#L42", | |
| 797 | + | "repo": "acme/web", | |
| 798 | + | "private": false, | |
| 799 | + | "description": null, | |
| 800 | + | "snippet": [], | |
| 801 | + | "lines": [ | |
| 802 | + | { | |
| 803 | + | "number": 41, | |
| 804 | + | "parts": [ | |
| 805 | + | { | |
| 806 | + | "text": "/// Reads a query typed into the search box.", | |
| 807 | + | "highlight": false | |
| 808 | + | } | |
| 809 | + | ] | |
| 810 | + | }, | |
| 811 | + | { | |
| 812 | + | "number": 42, | |
| 813 | + | "parts": [ | |
| 814 | + | { | |
| 815 | + | "text": "pub fn ", | |
| 816 | + | "highlight": false | |
| 817 | + | }, | |
| 818 | + | { | |
| 819 | + | "text": "parse_query", | |
| 820 | + | "highlight": true | |
| 821 | + | }, | |
| 822 | + | { | |
| 823 | + | "text": "(text: &str) -> Query {", | |
| 824 | + | "highlight": false | |
| 825 | + | } | |
| 826 | + | ] | |
| 827 | + | }, | |
| 828 | + | { | |
| 829 | + | "number": 43, | |
| 830 | + | "parts": [ | |
| 831 | + | { | |
| 832 | + | "text": " let mut query = Query::default();", | |
| 833 | + | "highlight": false | |
| 834 | + | } | |
| 835 | + | ] | |
| 836 | + | } | |
| 837 | + | ], | |
| 838 | + | "path": "src/search/query.rs", | |
| 839 | + | "language": "rust", | |
| 840 | + | "ref": "main", | |
| 841 | + | "number": null, | |
| 842 | + | "state": null, | |
| 843 | + | "author": null, | |
| 844 | + | "labels": [], | |
| 845 | + | "topics": [], | |
| 846 | + | "slug": null, | |
| 847 | + | "avatar": null, | |
| 848 | + | "updated_at": null | |
| 849 | + | } | |
| 850 | + | ], | |
| 851 | + | "notes": [] | |
| 852 | + | } | |
| 853 | + | } | |
| 854 | + | } | |
| 855 | + | }, | |
| 856 | + | "401": { | |
| 857 | + | "description": "A token is required, or the one sent is not valid.", | |
| 858 | + | "content": { | |
| 859 | + | "application/json": { | |
| 860 | + | "schema": { | |
| 861 | + | "$ref": "#/components/schemas/Error" | |
| 862 | + | } | |
| 863 | + | } | |
| 864 | + | } | |
| 865 | + | }, | |
| 866 | + | "403": { | |
| 867 | + | "description": "Signed in, but not allowed to do this.", | |
| 868 | + | "content": { | |
| 869 | + | "application/json": { | |
| 870 | + | "schema": { | |
| 871 | + | "$ref": "#/components/schemas/Error" | |
| 872 | + | } | |
| 873 | + | } | |
| 874 | + | } | |
| 875 | + | }, | |
| 876 | + | "422": { | |
| 877 | + | "description": "The input is not valid.", | |
| 878 | + | "content": { | |
| 879 | + | "application/json": { | |
| 880 | + | "schema": { | |
| 881 | + | "$ref": "#/components/schemas/Error" | |
| 882 | + | } | |
| 883 | + | } | |
| 884 | + | } | |
| 885 | + | } | |
| 886 | + | }, | |
| 887 | + | "x-example-query": { | |
| 888 | + | "q": "parse_query language:rust repo:acme/web", | |
| 889 | + | "type": "code" | |
| 890 | + | } | |
| 891 | + | } | |
| 892 | + | }, | |
| 706 | 893 | "/repos/{owner}/{name}": { | |
| 707 | 894 | "get": { | |
| 708 | 895 | "operationId": "get_repo", | |
| 807 | 994 | "Repositories" | |
| 808 | 995 | ], | |
| 809 | 996 | "summary": "Update a repository", | |
| 810 | − | "description": "Change a repository's description, whether it is private, and whether its default branch is protected. A protected branch refuses pushes and changes only by merging a pull request. Only the fields given are changed. Members of its workspace only.", | |
| 997 | + | "description": "Change a repository's description, its topics, whether it is private, and whether its default branch is protected. A protected branch refuses pushes and changes only by merging a pull request. Only the fields given are changed. Members of its workspace only.", | |
| 811 | 998 | "x-mcp-tool": "update_repo", | |
| 812 | 999 | "security": [ | |
| 813 | 1000 | { | |
| 923 | 1110 | "protected": { | |
| 924 | 1111 | "type": "boolean", | |
| 925 | 1112 | "description": "Refuse pushes to the default branch, so that it changes only by merging a pull request." | |
| 1113 | + | }, | |
| 1114 | + | "topics": { | |
| 1115 | + | "type": "array", | |
| 1116 | + | "items": { | |
| 1117 | + | "type": "string" | |
| 1118 | + | }, | |
| 1119 | + | "description": "Replaces its topics, which search and Explore show: lowercase letters, digits and hyphens, at most 20. An empty list clears them." | |
| 926 | 1120 | } | |
| 927 | 1121 | } | |
| 928 | 1122 | }, |
| 1 | + | import { Command as Cmdk } from "cmdk"; | |
| 2 | + | import { Code2, CornerDownLeft, Search } from "lucide-react"; | |
| 3 | + | import { Dialog as Primitive } from "radix-ui"; | |
| 4 | + | import { type ReactNode, useEffect, useRef, useState } from "react"; | |
| 5 | + | import { useFetcher, useNavigate } from "react-router"; | |
| 6 | + | ||
| 7 | + | import type { SiteHit } from "@g1t/contracts"; | |
| 8 | + | ||
| 9 | + | import { searchHref } from "../lib/search"; | |
| 10 | + | import { hitIcon } from "./search"; | |
| 11 | + | import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "./ui/combobox"; | |
| 12 | + | ||
| 13 | + | /** A page or action the palette can jump to. */ | |
| 14 | + | export type PaletteCommand = { label: string; hint?: string; to: string; icon: ReactNode }; | |
| 15 | + | ||
| 16 | + | /** How long typing has to pause before results are asked for. */ | |
| 17 | + | const PAUSE_MS = 140; | |
| 18 | + | ||
| 19 | + | /** Opens and closes the palette on ⌘K or Ctrl-K, from anywhere on the page. */ | |
| 20 | + | export function usePaletteShortcut(toggle: () => void) { | |
| 21 | + | const latest = useRef(toggle); | |
| 22 | + | latest.current = toggle; | |
| 23 | + | useEffect(() => { | |
| 24 | + | const onKey = (event: KeyboardEvent) => { | |
| 25 | + | if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { | |
| 26 | + | event.preventDefault(); | |
| 27 | + | latest.current(); | |
| 28 | + | } | |
| 29 | + | }; | |
| 30 | + | window.addEventListener("keydown", onKey); | |
| 31 | + | return () => window.removeEventListener("keydown", onKey); | |
| 32 | + | }, []); | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | function hitHint(hit: SiteHit): string { | |
| 36 | + | switch (hit.kind) { | |
| 37 | + | case "repository": | |
| 38 | + | return "Repository"; | |
| 39 | + | case "issue": | |
| 40 | + | return `${hit.repo} #${hit.number}`; | |
| 41 | + | case "pull": | |
| 42 | + | return `${hit.repo} #${hit.number}`; | |
| 43 | + | case "user": | |
| 44 | + | return `Person · ${hit.slug}`; | |
| 45 | + | case "workspace": | |
| 46 | + | return `Workspace · ${hit.slug}`; | |
| 47 | + | default: | |
| 48 | + | return hit.repo ?? ""; | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | /** | |
| 53 | + | * ⌘K: pages and actions from what the page already knows, and, as someone | |
| 54 | + | * types, repositories, issues, pull requests and people from search, | |
| 55 | + | * public ones for anyone and private ones for their workspaces' members. | |
| 56 | + | * The first row always searches all of g1t for what was typed. | |
| 57 | + | */ | |
| 58 | + | export function CommandPalette({ | |
| 59 | + | open, | |
| 60 | + | onOpenChange, | |
| 61 | + | commands, | |
| 62 | + | repo, | |
| 63 | + | }: { | |
| 64 | + | open: boolean; | |
| 65 | + | onOpenChange: (open: boolean) => void; | |
| 66 | + | commands: PaletteCommand[]; | |
| 67 | + | /** The repository being looked at, `owner/name`, to offer searching its code. */ | |
| 68 | + | repo?: string | null; | |
| 69 | + | }) { | |
| 70 | + | const navigate = useNavigate(); | |
| 71 | + | const fetcher = useFetcher<{ q: string; hits: SiteHit[] }>(); | |
| 72 | + | const [query, setQuery] = useState(""); | |
| 73 | + | const trimmed = query.trim(); | |
| 74 | + | ||
| 75 | + | useEffect(() => { | |
| 76 | + | if (open) setQuery(""); | |
| 77 | + | }, [open]); | |
| 78 | + | useEffect(() => { | |
| 79 | + | if (trimmed.length < 2) return; | |
| 80 | + | const timer = setTimeout(() => fetcher.load(`/search.json?q=${encodeURIComponent(trimmed)}`), PAUSE_MS); | |
| 81 | + | return () => clearTimeout(timer); | |
| 82 | + | // `fetcher` changes identity as it loads; only the text should restart it. | |
| 83 | + | // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 84 | + | }, [trimmed]); | |
| 85 | + | ||
| 86 | + | const hits = trimmed.length >= 2 && fetcher.data?.q === trimmed ? fetcher.data.hits : []; | |
| 87 | + | const go = (to: string) => { | |
| 88 | + | onOpenChange(false); | |
| 89 | + | // The docs and other sites are full page loads. | |
| 90 | + | if (/^https?:\/\//.test(to)) window.location.assign(to); | |
| 91 | + | else navigate(to); | |
| 92 | + | }; | |
| 93 | + | const words = trimmed.toLowerCase().split(/\s+/).filter(Boolean); | |
| 94 | + | const pages = commands | |
| 95 | + | .filter((command) => { | |
| 96 | + | const text = `${command.label} ${command.hint ?? ""}`.toLowerCase(); | |
| 97 | + | return words.every((word) => text.includes(word)); | |
| 98 | + | }) | |
| 99 | + | .slice(0, trimmed ? 8 : 12); | |
| 100 | + | const itemClass = "gap-3 rounded-lg px-3 py-2"; | |
| 101 | + | ||
| 102 | + | return ( | |
| 103 | + | <Primitive.Root open={open} onOpenChange={onOpenChange}> | |
| 104 | + | <Primitive.Portal> | |
| 105 | + | <Primitive.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out motion-reduce:animate-none" /> | |
| 106 | + | <Primitive.Content | |
| 107 | + | aria-describedby={undefined} | |
| 108 | + | className="fixed top-[12vh] left-1/2 z-50 w-[calc(100vw-2rem)] max-w-xl -translate-x-1/2 overflow-hidden rounded-xl border border-line-strong bg-raised shadow-2xl shadow-black/60 outline-none" | |
| 109 | + | > | |
| 110 | + | <Primitive.Title className="sr-only">Go to or search</Primitive.Title> | |
| 111 | + | {/* Filtering is done here: search already filtered its results. */} | |
| 112 | + | <Command shouldFilter={false} loop> | |
| 113 | + | <div className="flex items-center gap-3 border-b border-line px-4"> | |
| 114 | + | <Search size={16} className="shrink-0 text-faint" /> | |
| 115 | + | <Cmdk.Input | |
| 116 | + | value={query} | |
| 117 | + | onValueChange={setQuery} | |
| 118 | + | placeholder="Search g1t, or go to a page…" | |
| 119 | + | autoComplete="off" | |
| 120 | + | data-1p-ignore | |
| 121 | + | className="h-12 grow border-0 bg-transparent text-sm shadow-none outline-none placeholder:text-faint focus-visible:outline-none" | |
| 122 | + | /> | |
| 123 | + | <kbd className="rounded border border-line px-1.5 font-mono text-[0.6875rem] text-faint">esc</kbd> | |
| 124 | + | </div> | |
| 125 | + | <CommandList className="max-h-[55vh] p-1.5"> | |
| 126 | + | <CommandEmpty>Nothing matches.</CommandEmpty> | |
| 127 | + | {trimmed && ( | |
| 128 | + | <CommandGroup heading="Search"> | |
| 129 | + | <CommandItem value="search:all" onSelect={() => go(searchHref(trimmed))} className={itemClass}> | |
| 130 | + | <Search /> | |
| 131 | + | <span className="min-w-0 grow truncate"> | |
| 132 | + | Search g1t for <span className="font-medium text-fg">“{trimmed}”</span> | |
| 133 | + | </span> | |
| 134 | + | <CornerDownLeft className="size-3.5! text-faint!" /> | |
| 135 | + | </CommandItem> | |
| 136 | + | <CommandItem value="search:code" onSelect={() => go(searchHref(trimmed, "code"))} className={itemClass}> | |
| 137 | + | <Code2 /> | |
| 138 | + | <span className="min-w-0 grow truncate"> | |
| 139 | + | Search code for <span className="font-medium text-fg">“{trimmed}”</span> | |
| 140 | + | </span> | |
| 141 | + | </CommandItem> | |
| 142 | + | {repo && ( | |
| 143 | + | <CommandItem | |
| 144 | + | value="search:repo" | |
| 145 | + | onSelect={() => go(searchHref(`${trimmed} repo:${repo}`, "code"))} | |
| 146 | + | className={itemClass} | |
| 147 | + | > | |
| 148 | + | <Code2 /> | |
| 149 | + | <span className="min-w-0 grow truncate"> | |
| 150 | + | Search <span className="font-mono">{repo}</span> for{" "} | |
| 151 | + | <span className="font-medium text-fg">“{trimmed}”</span> | |
| 152 | + | </span> | |
| 153 | + | </CommandItem> | |
| 154 | + | )} | |
| 155 | + | </CommandGroup> | |
| 156 | + | )} | |
| 157 | + | {hits.length > 0 && ( | |
| 158 | + | <CommandGroup heading="Results"> | |
| 159 | + | {hits.map((hit) => ( | |
| 160 | + | <CommandItem | |
| 161 | + | key={`${hit.kind}:${hit.url}`} | |
| 162 | + | value={`hit:${hit.kind}:${hit.url}`} | |
| 163 | + | onSelect={() => go(hit.url)} | |
| 164 | + | className={itemClass} | |
| 165 | + | > | |
| 166 | + | {hitIcon(hit)} | |
| 167 | + | <span className="min-w-0 grow truncate"> | |
| 168 | + | {hit.kind === "repository" ? <span className="font-mono">{hit.title}</span> : hit.title} | |
| 169 | + | </span> | |
| 170 | + | <span className="shrink-0 truncate text-xs text-faint">{hitHint(hit)}</span> | |
| 171 | + | </CommandItem> | |
| 172 | + | ))} | |
| 173 | + | </CommandGroup> | |
| 174 | + | )} | |
| 175 | + | {pages.length > 0 && ( | |
| 176 | + | <CommandGroup heading="Go to"> | |
| 177 | + | {pages.map((command) => ( | |
| 178 | + | <CommandItem | |
| 179 | + | key={`${command.to}-${command.label}`} | |
| 180 | + | value={`page:${command.to}:${command.label}`} | |
| 181 | + | onSelect={() => go(command.to)} | |
| 182 | + | className={itemClass} | |
| 183 | + | > | |
| 184 | + | <span className="shrink-0 text-faint">{command.icon}</span> | |
| 185 | + | <span className="min-w-0 grow truncate">{command.label}</span> | |
| 186 | + | {command.hint && <span className="shrink-0 truncate text-xs text-faint">{command.hint}</span>} | |
| 187 | + | </CommandItem> | |
| 188 | + | ))} | |
| 189 | + | </CommandGroup> | |
| 190 | + | )} | |
| 191 | + | </CommandList> | |
| 192 | + | </Command> | |
| 193 | + | {hits.length === 0 && fetcher.state === "loading" && trimmed.length >= 2 && ( | |
| 194 | + | <p className="border-t border-line px-4 py-2 text-xs text-faint">Searching…</p> | |
| 195 | + | )} | |
| 196 | + | </Primitive.Content> | |
| 197 | + | </Primitive.Portal> | |
| 198 | + | </Primitive.Root> | |
| 199 | + | ); | |
| 200 | + | } |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.