feat(forge): pluggable forge abstraction + GitLab and Gitea/Forgejo/Codeberg (#1913)

* 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>
This commit is contained in:
nllptrx
2026-07-17 15:03:26 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent 04d1ebdce0
commit a8ebd390fa
187 changed files with 20988 additions and 2907 deletions
@@ -12,6 +12,7 @@ import type { AgentAttachment } from "@getpaseo/protocol/messages";
import type { WorkspaceComposerAttachment } from "@/attachments/types";
import { getFileTypeLabel } from "@/attachments/file-types";
import { isPullRequestContextAttachment } from "@/attachments/workspace-attachment-utils";
import { getForgePresentation } from "@/git/forge";
import { ICON_SIZE, type Theme } from "@/styles/theme";
export interface AttachmentPillContent {
@@ -27,10 +28,16 @@ function getReviewSubtitle(count: number, t: TFunction): string {
}
function getPullRequestContextSubtitle(attachment: WorkspaceComposerAttachment): string {
if (attachment.kind === "github.pull_request_check") {
if (
attachment.kind === "forge.change_request_check" ||
attachment.kind === "github.pull_request_check"
) {
return "Check logs";
}
if (attachment.kind === "github.pull_request_comment") {
if (
attachment.kind === "forge.change_request_comment" ||
attachment.kind === "github.pull_request_comment"
) {
return "Comment";
}
return "Review";
@@ -57,12 +64,21 @@ export function getAgentAttachmentPillContent(
title: t("message.attachments.review"),
subtitle: getReviewSubtitle(attachment.comments.length, t),
};
case "forge_change_request": {
const presentation = getForgePresentation(attachment.forge ?? "github");
return {
icon: attachmentGithubPrIcon,
title: attachment.title,
subtitle: `${presentation.changeRequestAbbrev} ${presentation.numberPrefix}${attachment.number}`,
};
}
case "github_pr":
return {
icon: attachmentGithubPrIcon,
title: attachment.title,
subtitle: `PR #${attachment.number}`,
};
case "forge_issue":
case "github_issue":
return {
icon: attachmentGithubIssueIcon,
+12 -3
View File
@@ -1,6 +1,6 @@
import type {
AgentAttachment,
GitHubSearchItem,
ForgeSearchItem,
UploadedFileAttachment,
} from "@getpaseo/protocol/messages";
@@ -54,6 +54,9 @@ export interface BrowserElementAttachment {
}
export type PullRequestContextAttachmentKind =
| "forge.change_request_comment"
| "forge.change_request_review"
| "forge.change_request_check"
| "github.pull_request_comment"
| "github.pull_request_review"
| "github.pull_request_check";
@@ -67,6 +70,9 @@ interface PullRequestContextAttachmentFields {
}
export type PullRequestContextAttachment =
| ({ kind: "forge.change_request_comment" } & PullRequestContextAttachmentFields)
| ({ kind: "forge.change_request_review" } & PullRequestContextAttachmentFields)
| ({ kind: "forge.change_request_check" } & PullRequestContextAttachmentFields)
| ({ kind: "github.pull_request_comment" } & PullRequestContextAttachmentFields)
| ({ kind: "github.pull_request_review" } & PullRequestContextAttachmentFields)
| ({ kind: "github.pull_request_check" } & PullRequestContextAttachmentFields);
@@ -89,10 +95,13 @@ export const NEW_WORKSPACE_PICKER_ATTACHMENT_OWNER = "new-workspace-picker";
export type UserComposerAttachment =
| { kind: "image"; metadata: AttachmentMetadata }
| { kind: "file"; attachment: UploadedFileAttachment }
| { kind: "github_issue"; item: GitHubSearchItem }
| { kind: "forge_issue"; item: ForgeSearchItem }
| { kind: "forge_change_request"; item: ForgeSearchItem }
// COMPAT(githubAttachmentKinds): added in v0.1.106, remove after 2026-12-28 once daemon floor >= v0.1.106
| { kind: "github_issue"; item: ForgeSearchItem }
| {
kind: "github_pr";
item: GitHubSearchItem;
item: ForgeSearchItem;
owner?: typeof NEW_WORKSPACE_PICKER_ATTACHMENT_OWNER;
};
@@ -10,6 +10,9 @@ export function isPullRequestContextAttachment(
attachment: ComposerAttachment | undefined,
): attachment is PullRequestContextAttachment {
return (
attachment?.kind === "forge.change_request_comment" ||
attachment?.kind === "forge.change_request_review" ||
attachment?.kind === "forge.change_request_check" ||
attachment?.kind === "github.pull_request_comment" ||
attachment?.kind === "github.pull_request_review" ||
attachment?.kind === "github.pull_request_check"
@@ -81,12 +81,15 @@ function areWorkspaceAttachmentsEqual(
}
function getContextAttachmentKey(attachment: WorkspaceComposerAttachment): string | null {
const isContextAttachment =
attachment.kind === "chat_history" ||
attachment.kind === "github.pull_request_comment" ||
attachment.kind === "github.pull_request_review" ||
attachment.kind === "github.pull_request_check";
if (!isContextAttachment) {
if (
attachment.kind !== "chat_history" &&
attachment.kind !== "forge.change_request_comment" &&
attachment.kind !== "forge.change_request_review" &&
attachment.kind !== "forge.change_request_check" &&
attachment.kind !== "github.pull_request_comment" &&
attachment.kind !== "github.pull_request_review" &&
attachment.kind !== "github.pull_request_check"
) {
return null;
}
return JSON.stringify({