63c21686ff
Co-authored-by: Kandev Agent <agent@kandev.dev>
158 lines
5.3 KiB
TypeScript
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";
|
|
}
|