feat(app): search GitHub repositories when adding a project - #8329
feat(app): search GitHub repositories when adding a project#8329msegec wants to merge 30 commits into
Conversation
…oviders
Adds searchRepositories to the SourceControlProvider interface, the
unregistered-provider fallback, and the per-method remote context binder,
so every adapter answers the same shape.
Every adapter returns { supported: false, results: [] } for now. GitLab,
Bitbucket, and Azure DevOps keep that answer: this pass is GitHub only, and
returning it as data rather than an error stops a search-as-you-type field
raising a toast on every keystroke. GitHub's is a marked placeholder that a
real gh search repos call replaces next.
GitHubCli.searchRepositories runs `gh repo list` for the viewer's own repositories and `gh search repos` for public ones, then merges them. The two commands disagree on field names (stargazerCount vs stargazersCount) and search returns no ssh URL, so each has its own schema and the ssh URL is derived from the repository's URL host. The query is free user text and the first client input in this repo to reach gh argv. GitHubCli strips anything outside [A-Za-z0-9._/-], drops leading dashes so a query can never be read as a flag, and caps the result at 128 characters.
The GitHub adapter's searchRepositories was a placeholder that always answered supported: false. It now calls the GitHub CLI search and returns supported: true with the results ranked own repositories first, prefix matches ahead of substring matches, then most stars. Results are capped at 20 rows and descriptions trimmed to 160 characters, because this payload is rebuilt on every keystroke.
Search-as-you-type spent two gh spawns per keystroke, and GitHub allows only 30 search requests a minute against 5000 an hour for the core API. The viewer's own repositories are now listed at most once a minute per working directory and matched locally, the global search runs only for queries of three characters or more that local matches do not already fill, and its rows are cached for 30 seconds per query. Both fetches go through SourceControlRateLimit; an open circuit serves cached rows or an empty list rather than raising a toast per keystroke. Also swaps forksCount for isFork in the gh search repos argv. The contract field is whether a repository is a fork, not how many forks it has, so the requested field never decoded onto it.
Repository search reached the provider boundary but had no way in from a client. Add searchRepositories to SourceControlRepositoryService, register the existing search RPC in WsRpcGroup, give it orchestration:read so standard pairing clients can call it, and wire the websocket handler.
Cloning a project required typing an exact owner/repo path, so a typo or a half-remembered name produced a lookup failure and no suggestions. The repository step now subscribes to the repository search query as the user types, debounced at 200ms with a two character minimum, and lists results in a "Your repositories" group and a provider-named group. Selecting a row skips the lookup round trip, since a result already carries the clone URLs. Enter still runs the exact-path lookup unless a repository row is highlighted, mirroring how a highlighted browse row takes Enter in the destination step. A provider that cannot search and a paused rate-limit circuit both render as empty-state text rather than an error.
Adding a cloned project on mobile required typing an exact owner/repo path, so a typo or a half-remembered name produced a lookup failure and no suggestions. The repository step now subscribes to the repository search query as the user types, debounced at 200ms with a two character minimum, and lists results in a "Your repositories" group and a provider-named group. Selecting a row pushes straight to the destination step, since a result already carries both clone URLs. Submitting the input still runs the exact-path lookup. A provider that cannot search and a paused rate-limit circuit both render as one line of text rather than an error.
The clone flow documented exact-path entry only. The repository step now searches GitHub by name, so describe that, and say plainly that GitLab, Bitbucket, and Azure DevOps still take an exact path.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
There was a problem hiding this comment.
Two Effect-convention issues in apps/server/src/sourceControl/GitHubCli.ts; everything else in the new search path (namespace subpath imports, Context.Service tag with inline interface, make/layer order, dependencies acquired via yield* SourceControlRateLimit.SourceControlRateLimit, structured Schema.TaggedErrorClass failure with a static detail and preserved cause, redacted query at the provider boundary, focused tests for the new behavior) matches the conventions.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
One layout finding on the repository step of the command palette. Everything else (shared item/group primitives, icon class, empty-state routing through CommandPaletteResults, explicit environmentId scoping of the search) looks consistent with the existing palette contracts.
Posted via Macroscope — UI Consistency
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR introduces a cross-cutting GitHub search workflow across web, mobile, RPC contracts, server-side CLI execution, caching, and rate-limit handling. It also changes RPC authorization configuration, so the scope and runtime effects require human review. You can add or adjust custom eligibility rules. Learn more. |
There was a problem hiding this comment.
UI Consistency
One finding on the new repository-search rows in the command palette. The input end-padding reserve flagged in the previous run (pe-32 vs. the widened ⌘ Enter accessory) is resolved by the new pe-38 branch, and the Enter/highlight/footer wiring now mirrors the browse step (autoHighlight={false} in the clone flow, handleQueryChange clearing the highlight, primary-modifier escape hatch back to the exact-path lookup), so no further findings there.
Posted via Macroscope — UI Consistency
Effect memoizes layers by reference, so the search layer, the pull-request service, and source control discovery were all built over one shared SourceControlRateLimit instance, keyed by the same provider and host. One gh search rate limit paused PR listing for its whole cooldown, and a PR rate limit silently blanked search. Layer.fresh gives the search layer the independent circuit its comment already promised. Also mark a searched repository as the viewer's own by comparing the owner segment of its name against the viewer's login taken from the owned listing, instead of membership in that listing, which gh caps at 100 rows. And treat an empty description from gh as absent so it is not forwarded over the wire.
A failed refresh after the 30 second search TTL answered with no public rows for one keystroke, then the open circuit brought the cached rows back. The failure path now serves the stale cached rows, falling back to local matches only when nothing is cached.
|
I am so sorry the demo images are not accurate, something about the dev server on my bazzite install renders funky, in actual instances across my fleet - it works great! Thanks again team. |
A typo in the add-project GitHub path surfaced as 'The source control operation could not be completed.' because gh's repository resolution failure was classified as a generic command failure and the service replaced every provider detail with that sentence. gh stderr saying 'could not resolve to a repository' now classifies as repository-not-found, GitHubCli maps it to a dedicated error, and the provider opts curated constant details into the client-facing message through a new userDetail field. Free-text provider detail still never reaches the wire.
Enter with no highlighted row always ran the exact-path lookup, so a search term like "effect" errored with "Repository not found" while its results were still loading. A bare term now stays with the search; path-shaped input, sources without a live search, and the primary modifier still look up.
There was a problem hiding this comment.
One finding on the repository step: the accessory button advertises Enter (label, Kbd, tooltip, and aria-label) as running the lookup, but Enter is now swallowed for a bare search term, so the keyboard and the click path disagree.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cee8af8. Configure here.
… the search The Lookup accessory kept promising Enter while a bare search term made plain Enter a no-op. The accessory, its width reservations, and the key handler now share one plain-Enter decision, so the shortcut label switches to the modifier chord exactly when that is the key that runs the lookup.
There was a problem hiding this comment.
One finding on the repository step's Enter affordances: the input accessory now advertises the modifier when plain Enter stays with the search, but the palette footer hint still names plain Enter for the lookup.
Posted via Macroscope — UI Consistency
|
Lesson learnt on this one, test a bit more before bother pushing a PR. my bad. |

Adding a project by cloning required an exact owner/repo path. A typo meant a hard failure toast and no suggestions.
The GitHub source in the add-project flow now searches as you type. Your own repositories are cached per environment and matched locally, so most keystrokes cost no network. Global search through gh search repos kicks in at 3 or more characters when local matches are thin, behind its own 30 per minute rate limit circuit that degrades to cached results instead of failing. GitLab, Bitbucket, and Azure DevOps report search as unsupported and keep the exact path input. Covers web and mobile over a new search RPC in contracts.
Verification: focused tests across contracts (7), server (74), web (28), mobile (14), plus per package typecheck. One integrated pass in the web client: own repo matches at 2 characters, global results at 3 or more, selection lands on Create & Clone.
Video walkthrough (38s): [watch in browser (webm)]
Screencast_20260827_163353.webm
or download mp4. Own repo matches while typing, global results, selection through Create & Clone, and the no-match empty state.
Written by Claude Fable 5 in Claude Code.
Note
Medium Risk
Touches source-control RPC/auth, shells out to
ghwith user queries (mitigated by sanitization and circuits), and changes add-project navigation races; scope is mostly read-only search but spans server, contracts, web, and mobile.Overview
Adds search-as-you-type when cloning a remote repo on web (command palette) and mobile (add project), backed by a new
sourceControl.searchRepositoriesRPC and client atoms/hooks.Server: GitHub search runs
gh repo list(cached, local substring match) and optionallygh search repos(3+ char queries, capped ranking/descriptions). Queries are sanitized; a dedicated rate-limit circuit returns cached/empty data instead of errors when paused. GitLab, Bitbucket, and Azure DevOps answer{ supported: false }.SourceControlDiscoveryis built once per server so caches/circuits are shared across WebSocket connections.Clients: Debounced queries (min 2 chars), grouped “Your repositories” vs provider, pick a row to skip lookup, stale exact-path lookups are ignored, and Enter/shortcuts distinguish select vs path lookup vs continue search. Lookup failures can surface curated
userDetail;ghrepo-not-found is classified separately.Reviewed by Cursor Bugbot for commit 75452e4. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add GitHub repository search to the Add Project flow
sourceControl.searchRepositoriesRPC (rpc.ts, sourceControl.ts) and wires it throughSourceControlRepositoryService, the websocket layer, and client state atoms.gh, ranks results by ownership, prefix match, and stars, caps at 20, trims descriptions to 160 chars, and caches results with TTL. Sanitizes queries to prevent flag injection. Falls back to cached or empty results when the rate-limit circuit is open.{ supported: false, results: [] }as data rather than failing.repository-not-foundfailure kind forghrepo resolution errors and surfaces curateduserDetailfromSourceControlProviderErrorin service error responses.SourceControlProviderErrorgains an optionaluserDetailfield; all providers must implementsearchRepositorieson theSourceControlProviderinterface. The GitHubCli layer now uses a freshSourceControlRateLimitinstance scoped to the CLI.Macroscope summarized 75452e4.