Files
2026-08-16 12:08:59 +01:00

158 lines
5.3 KiB
TypeScript

import type {
CreateIssueWatchRequest,
CreateReviewWatchRequest,
} from "@/lib/api/domains/gitlab-api";
import type { IssueWatch, ReviewWatch } from "@/lib/types/gitlab";
export type GitLabWatchKind = "review" | "issue";
export type GitLabWatchForm = {
workspaceId: string;
workflowId: string;
workflowStepId: string;
projectPaths: string;
repositoryId: string;
baseBranch: string;
agentProfileId: string;
executorProfileId: string;
prompt: string;
customQuery: string;
reviewScope: "user" | "user_and_teams";
labels: string;
pollIntervalSeconds: string;
maxInflightTasks: string;
cleanupPolicy: "auto" | "always" | "never";
};
// Seeded into the dialog's prompt field, then persisted on the watch and sent to
// the agent verbatim. Deliberately NOT translated: a watch created under one
// locale would keep that locale's prompt forever, and the agent reads it as an
// instruction rather than as UI copy. Same contract as
// DEFAULT_ISSUE_WATCH_PROMPT in components/github/issue-watch-placeholders.ts.
// i18n-exempt: default prompt, persisted and sent verbatim to the agent.
const DEFAULT_REVIEW_PROMPT =
"Review GitLab merge request {{mr.url}}. Summarize risks and leave actionable feedback.";
// i18n-exempt: default prompt, persisted and sent verbatim to the agent.
const DEFAULT_ISSUE_PROMPT =
"Investigate GitLab issue {{issue.url}} and implement the requested change.";
export function makeWatchForm(kind: GitLabWatchKind, workspaceId: string): GitLabWatchForm {
return {
workspaceId,
workflowId: "",
workflowStepId: "",
projectPaths: "",
repositoryId: "",
baseBranch: "",
agentProfileId: "",
executorProfileId: "",
prompt: kind === "review" ? DEFAULT_REVIEW_PROMPT : DEFAULT_ISSUE_PROMPT,
customQuery: "",
reviewScope: "user",
labels: "",
pollIntervalSeconds: "300",
maxInflightTasks: "",
cleanupPolicy: "auto",
};
}
export function watchFormFromWatch(
kind: GitLabWatchKind,
watch: ReviewWatch | IssueWatch,
): GitLabWatchForm {
const form = makeWatchForm(kind, watch.workspace_id);
return {
...form,
workflowId: watch.workflow_id,
workflowStepId: watch.workflow_step_id,
projectPaths: watch.projects.map((project) => project.path).join(", "),
repositoryId: watch.repository_id ?? "",
baseBranch: watch.base_branch ?? "",
agentProfileId: watch.agent_profile_id,
executorProfileId: watch.executor_profile_id,
prompt: watch.prompt || form.prompt,
customQuery: watch.custom_query,
reviewScope:
kind === "review" ? normalizeReviewScope((watch as ReviewWatch).review_scope) : "user",
labels: kind === "issue" ? ((watch as IssueWatch).labels ?? []).join(", ") : "",
pollIntervalSeconds: String(watch.poll_interval_seconds),
maxInflightTasks: watch.max_inflight_tasks ? String(watch.max_inflight_tasks) : "",
cleanupPolicy: normalizeCleanupPolicy(watch.cleanup_policy),
};
}
export function buildWatchPayload(
kind: "review",
form: GitLabWatchForm,
editing?: boolean,
): CreateReviewWatchRequest | null;
export function buildWatchPayload(
kind: "issue",
form: GitLabWatchForm,
editing?: boolean,
): CreateIssueWatchRequest | null;
export function buildWatchPayload(
kind: GitLabWatchKind,
form: GitLabWatchForm,
editing = false,
): CreateReviewWatchRequest | CreateIssueWatchRequest | null {
const pollInterval = Number(form.pollIntervalSeconds);
const hasInflightInput = Boolean(form.maxInflightTasks.trim());
let inflight: number | undefined;
if (hasInflightInput) inflight = Number(form.maxInflightTasks);
else if (editing) inflight = 0;
if (!isValidForm(form, pollInterval, inflight, hasInflightInput)) return null;
const common = {
workspace_id: form.workspaceId,
workflow_id: form.workflowId,
workflow_step_id: form.workflowStepId,
projects: parseList(form.projectPaths).map((path) => ({ path })),
repository_id: form.repositoryId,
base_branch: form.baseBranch,
agent_profile_id: form.agentProfileId,
executor_profile_id: form.executorProfileId,
prompt: form.prompt.trim(),
custom_query: form.customQuery.trim(),
poll_interval_seconds: pollInterval,
cleanup_policy: form.cleanupPolicy,
max_inflight_tasks: inflight,
};
if (kind === "review") return { ...common, review_scope: form.reviewScope };
return { ...common, labels: parseList(form.labels) };
}
function isValidForm(
form: GitLabWatchForm,
pollInterval: number,
inflight: number | undefined,
hasInflightInput: boolean,
): boolean {
const dependenciesReady = Boolean(
form.workspaceId && form.workflowId && form.workflowStepId && form.prompt.trim(),
);
const pollIntervalValid =
Number.isInteger(pollInterval) && pollInterval >= 60 && pollInterval <= 3600;
const inflightValid = !hasInflightInput || (Number.isInteger(inflight) && (inflight ?? 0) >= 1);
return dependenciesReady && pollIntervalValid && inflightValid;
}
function parseList(value: string): string[] {
return Array.from(
new Set(
value
.split(",")
.map((item) => item.trim())
.filter(Boolean),
),
);
}
function normalizeReviewScope(value: string): GitLabWatchForm["reviewScope"] {
return value === "user_and_teams" ? value : "user";
}
function normalizeCleanupPolicy(value: string): GitLabWatchForm["cleanupPolicy"] {
if (value === "always" || value === "never") return value;
return "auto";
}