* refactor(forge): forge-neutral foundation (GitHub-only) Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged. - Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry. - GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved. - forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client). - createPullRequest reports typed CLI/auth errors instead of a generic message. - forge-resolver host/remote caches are LRU-bounded. - Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com. - Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology. - Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs. * feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals. - threadIsResolved is part of the neutral timeline item. - Pipeline load failures show an error instead of an empty section. - Manual pipeline jobs render as pending. - Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request). * feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg) One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts. - CI status aggregates commit statuses and Actions runs together. - Gitea's terminal "warning" state maps to failure on server and client. - Gitea Actions check details are reachable from the PR pane by workflowRunId. * refactor(forge): localize compatibility handling * test(forge): expect normalized GitLab facts --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
173 lines
4.9 KiB
TypeScript
173 lines
4.9 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import {
|
|
ForgeSearchItemSchema,
|
|
GitHubSearchItemSchema,
|
|
type ForgeAuthState,
|
|
type ForgeSearchItem,
|
|
ForgeSearchKind,
|
|
type ForgeSearchResponse,
|
|
type GitHubSearchResponse,
|
|
} from "@getpaseo/protocol/messages";
|
|
import { i18n } from "@/i18n/i18next";
|
|
import { useFetchQuery } from "@/data/query";
|
|
import { parseForgeAuthState } from "@/git/forge";
|
|
|
|
export const FORGE_SEARCH_STALE_TIME = 30_000;
|
|
|
|
export interface ForgeSearchPayload {
|
|
items: ForgeSearchItem[];
|
|
authState: ForgeAuthState;
|
|
error: string | null;
|
|
requestId: string;
|
|
}
|
|
|
|
interface ForgeSearchOptions {
|
|
cwd: string;
|
|
query: string;
|
|
limit?: number;
|
|
kinds?: ForgeSearchKind[];
|
|
}
|
|
|
|
interface LegacyGitHubSearchOptions {
|
|
cwd: string;
|
|
query: string;
|
|
limit?: number;
|
|
kinds?: LegacyGitHubSearchKind[];
|
|
}
|
|
|
|
export interface ForgeSearchClient {
|
|
searchForge: (
|
|
options: ForgeSearchOptions,
|
|
requestId?: string,
|
|
) => Promise<ForgeSearchResponse["payload"]>;
|
|
searchGitHub?: (
|
|
options: LegacyGitHubSearchOptions,
|
|
requestId?: string,
|
|
) => Promise<GitHubSearchResponse["payload"]>;
|
|
}
|
|
|
|
type LegacyGitHubSearchKind = "github-issue" | "github-pr";
|
|
|
|
interface ForgeSearchQueryInput {
|
|
client: ForgeSearchClient | null;
|
|
serverId: string;
|
|
cwd: string;
|
|
query: string;
|
|
kinds?: ForgeSearchKind[];
|
|
enabled: boolean;
|
|
supportsForgeSearch?: boolean;
|
|
hostDisconnectedMessage?: string;
|
|
}
|
|
|
|
export function forgeSearchQueryKey(
|
|
serverId: string,
|
|
cwd: string,
|
|
query: string,
|
|
kinds?: ForgeSearchKind[],
|
|
transport: "forge" | "github" = "forge",
|
|
) {
|
|
const trimmedQuery = query.trim();
|
|
if (!kinds) {
|
|
return ["forge-search", serverId, cwd, transport, trimmedQuery] as const;
|
|
}
|
|
return [
|
|
"forge-search",
|
|
serverId,
|
|
cwd,
|
|
transport,
|
|
trimmedQuery,
|
|
[...kinds].sort().join(","),
|
|
] as const;
|
|
}
|
|
|
|
export function buildForgeSearchQueryOptions(input: ForgeSearchQueryInput) {
|
|
const query = input.query.trim();
|
|
const transport = input.supportsForgeSearch === true ? "forge" : "github";
|
|
|
|
return {
|
|
queryKey: forgeSearchQueryKey(input.serverId, input.cwd, query, input.kinds, transport),
|
|
queryFn: async (): Promise<ForgeSearchPayload> => {
|
|
if (!input.client) {
|
|
throw new Error(
|
|
input.hostDisconnectedMessage ?? i18n.t("workspace.terminal.hostDisconnected"),
|
|
);
|
|
}
|
|
const request = input.kinds
|
|
? { cwd: input.cwd, query, limit: 20, kinds: input.kinds }
|
|
: { cwd: input.cwd, query, limit: 20 };
|
|
// COMPAT(githubSearchRpc): added in v0.1.106, remove after 2026-12-28 once
|
|
// clients use forge.search.*.
|
|
if (transport === "github" && input.client.searchGitHub) {
|
|
return normalizeLegacyGitHubSearchPayload(
|
|
await input.client.searchGitHub(toLegacyGitHubSearchRequest(request)),
|
|
);
|
|
}
|
|
return normalizeForgeSearchPayload(await input.client.searchForge(request));
|
|
},
|
|
enabled: input.enabled && Boolean(input.client),
|
|
dataShape: "list" as const,
|
|
staleTimeMs: FORGE_SEARCH_STALE_TIME,
|
|
};
|
|
}
|
|
|
|
function normalizeForgeSearchPayload(payload: ForgeSearchResponse["payload"]): ForgeSearchPayload {
|
|
return {
|
|
items: payload.items.flatMap((item) => {
|
|
const result = ForgeSearchItemSchema.safeParse(item);
|
|
return result.success ? [result.data] : [];
|
|
}),
|
|
authState: parseForgeAuthState(payload.authState) ?? "unauthenticated",
|
|
error: payload.error,
|
|
requestId: payload.requestId,
|
|
};
|
|
}
|
|
|
|
function normalizeLegacyGitHubSearchPayload(
|
|
payload: GitHubSearchResponse["payload"],
|
|
): ForgeSearchPayload {
|
|
// COMPAT(githubSearchAuthState): added in v0.1.106, remove after 2026-12-28.
|
|
const featuresEnabled = payload.featuresEnabled ?? payload.githubFeaturesEnabled ?? true;
|
|
return {
|
|
items: payload.items.flatMap((item) => {
|
|
const result = GitHubSearchItemSchema.safeParse(item);
|
|
if (!result.success) {
|
|
return [];
|
|
}
|
|
return [
|
|
result.data.kind === "pr"
|
|
? { ...result.data, kind: "change_request" as const }
|
|
: { ...result.data, kind: "issue" as const },
|
|
];
|
|
}),
|
|
authState:
|
|
parseForgeAuthState(payload.authState) ??
|
|
(featuresEnabled ? "authenticated" : "unauthenticated"),
|
|
error: payload.error,
|
|
requestId: payload.requestId,
|
|
};
|
|
}
|
|
|
|
function toLegacyGitHubSearchKind(kind: ForgeSearchKind): LegacyGitHubSearchKind {
|
|
return kind === "change_request" ? "github-pr" : "github-issue";
|
|
}
|
|
|
|
function toLegacyGitHubSearchRequest(request: ForgeSearchOptions): LegacyGitHubSearchOptions {
|
|
if (!request.kinds) {
|
|
return { cwd: request.cwd, query: request.query, limit: request.limit };
|
|
}
|
|
return {
|
|
...request,
|
|
kinds: request.kinds.map(toLegacyGitHubSearchKind),
|
|
};
|
|
}
|
|
|
|
export function useForgeSearchQuery(input: ForgeSearchQueryInput) {
|
|
const { t } = useTranslation();
|
|
return useFetchQuery(
|
|
buildForgeSearchQueryOptions({
|
|
...input,
|
|
hostDisconnectedMessage: t("workspace.terminal.hostDisconnected"),
|
|
}),
|
|
);
|
|
}
|