mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
* feat(protocol): add forge check presentation metadata Expose optional rawStatus, isManual, and requiresAction fields while keeping older check payloads backward-compatible. * fix(gitea): model Actions jobs as tasks Gitea's /actions/tasks endpoint returns individual jobs rather than workflow runs. Preserve sibling tasks from the latest execution, avoid collapsing same-name jobs, and enrich blocked tasks with run-level approval metadata. * feat(app): distinguish forge check presentation states * fix(gitlab): preserve pipeline job semantics * refactor(app): centralize forge check presentation policy Use one canonical category order, count shape, visual policy, and translated copy mapping across sidebar, hover-card, PR-pane, and GitLab pipeline surfaces. * refactor: consolidate forge check handling * feat(forge): auto-attach pasted forge references * docs(compat): correct forge rollout metadata * refactor(forge): model check nuances as traits * fix(forge): harden checks from branch review Structural Gitea Actions shadow dedup by run id (verified upstream), rerun grouping without workflow_id, shared trait/status constants and Gitea state mapping in the protocol package, all cloud host aliases in pasted-link matching, logged glab job-detail failures, concurrent approvals/pipeline fetch, checks aggregate from the same pipeline as the job list, and removal of the unread check-level rawStatus. The released pipeline-level rawStatus is staged for removal under COMPAT(pipelineRawStatus). * fix(gitea): detect fork-PR Actions approval hold as action_required gitea.com represents a fork PR awaiting Actions approval as a run in status "waiting" with no tasks and pending shadow commit statuses; it does not expose need_approval. The previous mapping only set action_required from a task with blocked+need_approval, so the gate never surfaced (0 tasks -> early return; need_approval absent). Detect the hold from /actions/runs: a waiting/blocked run whose PR is cross-repo (head.repo.id != base.repo.id), or one that does expose need_approval (self-hosted). Tag the run's pending shadow statuses action_required. Runs are only fetched when a pending shadow has no covering task, keeping the common path at two requests. Verified live against gitea.com fork PR (run 715170, waiting, cross-repo). * fix(gitea): resolve cross-fork PRs by head sha The current-PR matcher gated candidates on the checkout owner (giteaHeadOwner === expectedHeadOwner), which excludes every fork PR: its head carries the fork owner while the checkout is the base repo, so a fork PR - the only shape that can be approval-held on Gitea - never resolved and its checks never showed in-app. GitLab has no such gate (it resolves fork MRs from the target project by source branch). Accept a recent PR whose head sha equals the workspace head sha regardless of owner; the sha is an unambiguous identity, so the anti-collision owner gate still applies when no sha match is available. Verified live against gitea.com fork PR #6 (resolves + renders action_required). * fix: resolve forge rebase drift * fix: type forge auto-attach boundary * fix(i18n): add Korean check status copy --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
175 lines
5.1 KiB
TypeScript
175 lines
5.1 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): use the legacy GitHub RPC with daemons that
|
|
// predate forge.search.*. Remove after 2027-01-17 once the supported
|
|
// daemon floor is >= v0.2.0.
|
|
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): normalize the legacy GitHub search response
|
|
// until 2027-01-17, when the supported daemon floor is >= v0.2.0.
|
|
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"),
|
|
}),
|
|
);
|
|
}
|