Merge branch 'puter/issue-8-project-loop' into puter/issue-14-integration
# Conflicts: # apps/web/src/components/projects/project-workspace-page.tsx # packages/primitives/package.json # packages/primitives/src/index.ts
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"@code/auth": "workspace:*",
|
||||
"@code/backend": "workspace:*",
|
||||
"@code/env": "workspace:*",
|
||||
"@code/primitives": "workspace:*",
|
||||
"@code/ui": "workspace:*",
|
||||
"@flue/react": "1.0.0-beta.9",
|
||||
"@flue/sdk": "1.0.0-beta.9",
|
||||
@@ -40,7 +41,7 @@
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vite-tsconfig-paths": "^6.1.1"
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,11 @@ import { useFlueClient } from "@flue/react";
|
||||
import { useAction, useMutation, useQuery } from "convex/react";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
buildProjectLoopView,
|
||||
summarizeProjectIssues,
|
||||
} from "@/lib/projects/project-evidence";
|
||||
|
||||
const errorMessage = (error: unknown) =>
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
@@ -15,6 +20,8 @@ export const useProjectWorkspace = () => {
|
||||
const [repository, setRepository] = useState("");
|
||||
const [issueTitle, setIssueTitle] = useState("");
|
||||
const [issueBody, setIssueBody] = useState("");
|
||||
const [selectedIssueId, setSelectedIssueId] =
|
||||
useState<Id<"projectIssues"> | null>(null);
|
||||
const [pendingAction, setPendingAction] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const importPublicGit = useAction(api.projects.importPublicGit);
|
||||
@@ -79,11 +86,12 @@ export const useProjectWorkspace = () => {
|
||||
setPendingAction("issue");
|
||||
setError(null);
|
||||
try {
|
||||
await createIssue({
|
||||
const issueId = await createIssue({
|
||||
body: issueBody,
|
||||
projectId: activeProjectId,
|
||||
title: issueTitle,
|
||||
});
|
||||
setSelectedIssueId(issueId);
|
||||
setIssueTitle("");
|
||||
setIssueBody("");
|
||||
} catch (caughtError) {
|
||||
@@ -118,6 +126,14 @@ export const useProjectWorkspace = () => {
|
||||
projects?.find(
|
||||
(project) => project.id === (activeProjectId as unknown as string)
|
||||
) ?? null;
|
||||
const selectedIssue =
|
||||
issues?.find((issue) => issue._id === selectedIssueId) ?? issues?.[0];
|
||||
const projectLoop = buildProjectLoopView({
|
||||
artifacts,
|
||||
issue: selectedIssue,
|
||||
source: selectedProject?.sources[0],
|
||||
});
|
||||
const issueSummary = summarizeProjectIssues(issues);
|
||||
|
||||
return {
|
||||
artifacts,
|
||||
@@ -125,18 +141,23 @@ export const useProjectWorkspace = () => {
|
||||
error,
|
||||
events,
|
||||
issueBody,
|
||||
issueSummary,
|
||||
issueTitle,
|
||||
issues,
|
||||
pendingAction,
|
||||
projectLoop,
|
||||
projects,
|
||||
pullRequest,
|
||||
raiseIssue,
|
||||
repository,
|
||||
selectedIssue,
|
||||
selectedIssueId,
|
||||
selectedProject,
|
||||
selectedProjectId: activeProjectId,
|
||||
setIssueBody,
|
||||
setIssueTitle,
|
||||
setRepository,
|
||||
setSelectedIssueId,
|
||||
setSelectedProjectId,
|
||||
startIssue,
|
||||
} as const;
|
||||
|
||||
91
apps/web/src/lib/projects/project-evidence.test.ts
Normal file
91
apps/web/src/lib/projects/project-evidence.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { Doc, Id } from "@code/backend/convex/_generated/dataModel";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
buildProjectLoopView,
|
||||
summarizeProjectIssues,
|
||||
} from "./project-evidence";
|
||||
|
||||
const makeIssue = (
|
||||
status: Doc<"projectIssues">["status"],
|
||||
issueId: Id<"projectIssues"> = "issue-1" as Id<"projectIssues">
|
||||
): Doc<"projectIssues"> =>
|
||||
({
|
||||
_creationTime: 1,
|
||||
_id: issueId,
|
||||
body: "Make the project loop visible in the web app.",
|
||||
createdAt: 1000,
|
||||
number: 8,
|
||||
projectId: "project-1",
|
||||
status,
|
||||
title: "Build project loop UI",
|
||||
updatedAt: 2000,
|
||||
}) as Doc<"projectIssues">;
|
||||
|
||||
const makeArtifact = (content: string): Doc<"projectArtifacts"> =>
|
||||
({
|
||||
_creationTime: 1,
|
||||
_id: "artifact-1",
|
||||
content,
|
||||
createdAt: 1000,
|
||||
path: "artifacts.md",
|
||||
projectId: "project-1",
|
||||
revision: 2,
|
||||
updatedAt: 2000,
|
||||
}) as Doc<"projectArtifacts">;
|
||||
|
||||
describe("project evidence", () => {
|
||||
test("turns published PR evidence into a review action", () => {
|
||||
const view = buildProjectLoopView({
|
||||
artifacts: [
|
||||
makeArtifact(
|
||||
"Verification: 14 unit tests passed.\nPull request: PR #42"
|
||||
),
|
||||
],
|
||||
issue: makeIssue("completed"),
|
||||
source: {
|
||||
host: "git.example.com",
|
||||
repositoryPath: "puter/zopu",
|
||||
url: "https://git.example.com/puter/zopu",
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.progress).toBe(100);
|
||||
expect(view?.verification).toBe("passed");
|
||||
expect(view?.pullRequest.reviewUrl).toBe(
|
||||
"https://git.example.com/puter/zopu/pulls/42"
|
||||
);
|
||||
expect(view?.verificationNote).toContain(
|
||||
"Verification: 14 unit tests passed."
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps the review slot pending when no PR is published", () => {
|
||||
const view = buildProjectLoopView({
|
||||
artifacts: [
|
||||
makeArtifact("The run is recording implementation evidence."),
|
||||
],
|
||||
issue: makeIssue("working"),
|
||||
source: undefined,
|
||||
});
|
||||
|
||||
expect(view?.verification).toBe("running");
|
||||
expect(view?.pullRequest.state).toBe("pending");
|
||||
expect(view?.pullRequest.reviewUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
test("summarizes the project queue without inventing work", () => {
|
||||
const summary = summarizeProjectIssues([
|
||||
makeIssue("working"),
|
||||
makeIssue("needs-input", "issue-2" as Id<"projectIssues">),
|
||||
makeIssue("completed", "issue-3" as Id<"projectIssues">),
|
||||
]);
|
||||
|
||||
expect(summary).toEqual({
|
||||
active: 2,
|
||||
completed: 1,
|
||||
needsInput: 1,
|
||||
total: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
323
apps/web/src/lib/projects/project-evidence.ts
Normal file
323
apps/web/src/lib/projects/project-evidence.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import type { Doc } from "@code/backend/convex/_generated/dataModel";
|
||||
import type { ProjectIssueStatus } from "@code/primitives/project-issue";
|
||||
import {
|
||||
getProjectWorkProgress,
|
||||
getProjectWorkStatus,
|
||||
} from "@code/primitives/project-work";
|
||||
import type { ProjectVerificationStatus } from "@code/primitives/project-work";
|
||||
|
||||
type ProjectIssue = Doc<"projectIssues">;
|
||||
type ProjectArtifact = Doc<"projectArtifacts">;
|
||||
|
||||
export interface ProjectSourceSummary {
|
||||
readonly host: string;
|
||||
readonly repositoryPath: string;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export interface ProjectIssueSummary {
|
||||
readonly active: number;
|
||||
readonly completed: number;
|
||||
readonly needsInput: number;
|
||||
readonly total: number;
|
||||
}
|
||||
|
||||
export interface ProjectVerificationCheck {
|
||||
readonly detail: string;
|
||||
readonly label: string;
|
||||
readonly state: "attention" | "complete" | "current" | "upcoming";
|
||||
}
|
||||
|
||||
export interface ProjectActivityItem {
|
||||
readonly detail: string;
|
||||
readonly label: string;
|
||||
readonly time: string;
|
||||
}
|
||||
|
||||
export interface ProjectPullRequest {
|
||||
readonly number: number | undefined;
|
||||
readonly reviewUrl: string | undefined;
|
||||
readonly state: "available" | "pending";
|
||||
}
|
||||
|
||||
export interface ProjectLoopView {
|
||||
readonly activity: readonly ProjectActivityItem[];
|
||||
readonly currentState: string;
|
||||
readonly nextAction: string;
|
||||
readonly progress: number;
|
||||
readonly pullRequest: ProjectPullRequest;
|
||||
readonly status: ReturnType<typeof getProjectWorkStatus>;
|
||||
readonly verification: ProjectVerificationStatus;
|
||||
readonly verificationChecks: readonly ProjectVerificationCheck[];
|
||||
readonly verificationNote: string;
|
||||
}
|
||||
|
||||
const STATUS_TEXT: Readonly<Record<ProjectIssueStatus, string>> = {
|
||||
completed:
|
||||
"Implementation and verification are complete. Review the resulting change.",
|
||||
failed:
|
||||
"The run stopped with its workspace preserved. Review the failure and retry when ready.",
|
||||
"needs-input":
|
||||
"The run is paused until a project decision or missing piece is resolved.",
|
||||
open: "The issue is ready to start. Zopu will keep the project context attached to the run.",
|
||||
queued: "The issue is queued for the project manager to pick up.",
|
||||
working:
|
||||
"The project manager is working through the issue and recording durable evidence.",
|
||||
};
|
||||
|
||||
const NEXT_ACTION: Readonly<Record<ProjectIssueStatus, string>> = {
|
||||
completed: "Review the pull request",
|
||||
failed: "Review the failure and retry",
|
||||
"needs-input": "Resolve the open question",
|
||||
open: "Start the project manager",
|
||||
queued: "Watch the run progress",
|
||||
working: "Wait for verification",
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: number): string =>
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
}).format(timestamp);
|
||||
|
||||
const relativeTime = (timestamp: number): string => {
|
||||
const minutes = Math.max(0, Math.round((Date.now() - timestamp) / 60_000));
|
||||
if (minutes < 1) {
|
||||
return "just now";
|
||||
}
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m ago`;
|
||||
}
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 24) {
|
||||
return `${hours}h ago`;
|
||||
}
|
||||
return `${Math.round(hours / 24)}d ago`;
|
||||
};
|
||||
|
||||
const allArtifactText = (artifacts: readonly ProjectArtifact[]): string =>
|
||||
artifacts.map((artifact) => artifact.content).join("\n");
|
||||
|
||||
const extractPullRequest = (
|
||||
artifacts: readonly ProjectArtifact[],
|
||||
source: ProjectSourceSummary | undefined
|
||||
): ProjectPullRequest => {
|
||||
const text = allArtifactText(artifacts);
|
||||
const urlMatch = text.match(/https?:\/\/[^\s)]+\/pulls\/\d+/iu)?.[0];
|
||||
const numberMatch = text.match(/\bPR\s*#(?<number>\d+)\b/iu);
|
||||
const numberValue = numberMatch?.groups?.number;
|
||||
const number = numberValue ? Number(numberValue) : undefined;
|
||||
let reviewUrl = urlMatch;
|
||||
if (!reviewUrl && number && source) {
|
||||
reviewUrl = `${source.url.replace(/\/$/u, "")}/pulls/${number}`;
|
||||
}
|
||||
|
||||
return {
|
||||
number,
|
||||
reviewUrl,
|
||||
state: reviewUrl ? "available" : "pending",
|
||||
};
|
||||
};
|
||||
|
||||
const extractVerificationNote = (
|
||||
artifacts: readonly ProjectArtifact[],
|
||||
status: ProjectIssueStatus
|
||||
): string => {
|
||||
const evidenceLine = allArtifactText(artifacts)
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.find((line) => /verification|tests?|browser|passed|failed/iu.test(line));
|
||||
|
||||
if (evidenceLine) {
|
||||
return evidenceLine.replace(/^[-*#\s]+/u, "").slice(0, 140);
|
||||
}
|
||||
if (status === "completed") {
|
||||
return "The agent reported completion after its verification step.";
|
||||
}
|
||||
if (status === "failed") {
|
||||
return "Verification did not complete successfully.";
|
||||
}
|
||||
if (status === "working") {
|
||||
return "Verification will appear here as the run records evidence.";
|
||||
}
|
||||
return "No verification evidence has been recorded yet.";
|
||||
};
|
||||
|
||||
const makeRunCheck = (status: ProjectIssueStatus): ProjectVerificationCheck => {
|
||||
if (status === "queued") {
|
||||
return {
|
||||
detail: "Agent admission is queued",
|
||||
label: "Agent run",
|
||||
state: "current",
|
||||
};
|
||||
}
|
||||
if (status === "open") {
|
||||
return {
|
||||
detail: "Start the issue to create a run",
|
||||
label: "Agent run",
|
||||
state: "upcoming",
|
||||
};
|
||||
}
|
||||
return {
|
||||
detail: "Project manager run is attached",
|
||||
label: "Agent run",
|
||||
state: "complete",
|
||||
};
|
||||
};
|
||||
|
||||
const makeVerificationCheck = (
|
||||
verification: ProjectVerificationStatus
|
||||
): ProjectVerificationCheck => {
|
||||
if (verification === "passed") {
|
||||
return {
|
||||
detail: "Verification passed",
|
||||
label: "Verification",
|
||||
state: "complete",
|
||||
};
|
||||
}
|
||||
if (verification === "failed") {
|
||||
return {
|
||||
detail: "Review the preserved failure",
|
||||
label: "Verification",
|
||||
state: "attention",
|
||||
};
|
||||
}
|
||||
if (verification === "blocked") {
|
||||
return {
|
||||
detail: "Waiting on a project decision",
|
||||
label: "Verification",
|
||||
state: "attention",
|
||||
};
|
||||
}
|
||||
if (verification === "running") {
|
||||
return {
|
||||
detail: "Evidence is being recorded",
|
||||
label: "Verification",
|
||||
state: "current",
|
||||
};
|
||||
}
|
||||
return {
|
||||
detail: "No verification result yet",
|
||||
label: "Verification",
|
||||
state: "upcoming",
|
||||
};
|
||||
};
|
||||
|
||||
const makeChecks = (
|
||||
status: ProjectIssueStatus,
|
||||
verification: ProjectVerificationStatus,
|
||||
pullRequest: ProjectPullRequest
|
||||
): readonly ProjectVerificationCheck[] => {
|
||||
const issueCheck: ProjectVerificationCheck =
|
||||
status === "open"
|
||||
? {
|
||||
detail: "Waiting to be started",
|
||||
label: "Issue accepted",
|
||||
state: "upcoming",
|
||||
}
|
||||
: {
|
||||
detail: "Issue context is attached",
|
||||
label: "Issue accepted",
|
||||
state: "complete",
|
||||
};
|
||||
const pullRequestCheck: ProjectVerificationCheck =
|
||||
pullRequest.state === "available"
|
||||
? {
|
||||
detail: "Ready for human review",
|
||||
label: "Gitea pull request",
|
||||
state: "complete",
|
||||
}
|
||||
: {
|
||||
detail: "Appears after a verified run publishes one",
|
||||
label: "Gitea pull request",
|
||||
state: "upcoming",
|
||||
};
|
||||
return [
|
||||
issueCheck,
|
||||
makeRunCheck(status),
|
||||
makeVerificationCheck(verification),
|
||||
pullRequestCheck,
|
||||
];
|
||||
};
|
||||
|
||||
const buildActivity = (
|
||||
issue: ProjectIssue,
|
||||
artifacts: readonly ProjectArtifact[],
|
||||
statusLabel: string
|
||||
): readonly ProjectActivityItem[] => {
|
||||
const artifact = [...artifacts]
|
||||
.toSorted((left, right) => right.updatedAt - left.updatedAt)
|
||||
.find((candidate) => candidate.path !== "work.md");
|
||||
const items: ProjectActivityItem[] = [
|
||||
{
|
||||
detail: `Issue #${issue.number} entered the project loop`,
|
||||
label: "Issue created",
|
||||
time: formatTime(issue.createdAt),
|
||||
},
|
||||
];
|
||||
|
||||
if (issue.updatedAt !== issue.createdAt) {
|
||||
items.unshift({
|
||||
detail: `Status is now ${statusLabel.toLowerCase()}`,
|
||||
label: "Work state updated",
|
||||
time: relativeTime(issue.updatedAt),
|
||||
});
|
||||
}
|
||||
|
||||
if (artifact) {
|
||||
items.unshift({
|
||||
detail: `Durable project evidence changed in ${artifact.path}`,
|
||||
label: "Evidence recorded",
|
||||
time: relativeTime(artifact.updatedAt),
|
||||
});
|
||||
}
|
||||
|
||||
return items.slice(0, 3);
|
||||
};
|
||||
|
||||
export const summarizeProjectIssues = (
|
||||
issues: readonly ProjectIssue[] | undefined
|
||||
): ProjectIssueSummary => {
|
||||
if (!issues) {
|
||||
return { active: 0, completed: 0, needsInput: 0, total: 0 };
|
||||
}
|
||||
return {
|
||||
active: issues.filter(
|
||||
(issue) => !["completed", "failed"].includes(issue.status)
|
||||
).length,
|
||||
completed: issues.filter((issue) => issue.status === "completed").length,
|
||||
needsInput: issues.filter((issue) => issue.status === "needs-input").length,
|
||||
total: issues.length,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildProjectLoopView = ({
|
||||
artifacts,
|
||||
issue,
|
||||
source,
|
||||
}: {
|
||||
readonly artifacts: readonly ProjectArtifact[] | undefined;
|
||||
readonly issue: ProjectIssue | undefined;
|
||||
readonly source: ProjectSourceSummary | undefined;
|
||||
}): ProjectLoopView | null => {
|
||||
if (!issue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = getProjectWorkStatus(issue.status);
|
||||
const { verification } = status;
|
||||
const resolvedArtifacts = artifacts ?? [];
|
||||
const pullRequest = extractPullRequest(resolvedArtifacts, source);
|
||||
|
||||
return {
|
||||
activity: buildActivity(issue, resolvedArtifacts, status.label),
|
||||
currentState: STATUS_TEXT[issue.status],
|
||||
nextAction: NEXT_ACTION[issue.status],
|
||||
progress: getProjectWorkProgress(issue.status),
|
||||
pullRequest,
|
||||
status,
|
||||
verification,
|
||||
verificationChecks: makeChecks(issue.status, verification, pullRequest),
|
||||
verificationNote: extractVerificationNote(resolvedArtifacts, issue.status),
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
|
||||
"include": [
|
||||
"**/*",
|
||||
"**/.server/**/*",
|
||||
"**/.client/**/*",
|
||||
".react-router/types/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2024"],
|
||||
"types": ["node", "vite/client"],
|
||||
@@ -10,6 +15,8 @@
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@code/primitives": ["../../packages/primitives/src/index.ts"],
|
||||
"@code/primitives/*": ["../../packages/primitives/src/*"],
|
||||
"@code/ui/*": ["../../packages/ui/src/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"./project": "./src/project.ts",
|
||||
"./project-issue": "./src/project-issue.ts",
|
||||
"./project-workspace": "./src/project-workspace.ts",
|
||||
"./project-work": "./src/project-work.ts",
|
||||
"./signal": "./src/signal.ts"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -4,4 +4,5 @@ export * from "./git";
|
||||
export * from "./project";
|
||||
export * from "./project-issue";
|
||||
export * from "./project-workspace";
|
||||
export * from "./project-work";
|
||||
export * from "./signal";
|
||||
|
||||
30
packages/primitives/src/project-work.test.ts
Normal file
30
packages/primitives/src/project-work.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getProjectWorkProgress, getProjectWorkStatus } from "./project-work";
|
||||
|
||||
describe("project work state mapping", () => {
|
||||
test("maps issue and run statuses to the same product language", () => {
|
||||
expect(getProjectWorkStatus("working")).toEqual({
|
||||
label: "In progress",
|
||||
phase: "active",
|
||||
verification: "running",
|
||||
});
|
||||
expect(getProjectWorkStatus("needs-input")).toEqual({
|
||||
label: "Needs your input",
|
||||
phase: "waiting",
|
||||
verification: "blocked",
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps progress monotonic through the normal loop", () => {
|
||||
expect(getProjectWorkProgress("open")).toBeLessThan(
|
||||
getProjectWorkProgress("queued")
|
||||
);
|
||||
expect(getProjectWorkProgress("queued")).toBeLessThan(
|
||||
getProjectWorkProgress("working")
|
||||
);
|
||||
expect(getProjectWorkProgress("working")).toBeLessThan(
|
||||
getProjectWorkProgress("completed")
|
||||
);
|
||||
});
|
||||
});
|
||||
115
packages/primitives/src/project-work.ts
Normal file
115
packages/primitives/src/project-work.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { ProjectIssueStatus } from "./project-issue.js";
|
||||
|
||||
export const PROJECT_ISSUE_STATUSES = [
|
||||
"open",
|
||||
"queued",
|
||||
"working",
|
||||
"needs-input",
|
||||
"completed",
|
||||
"failed",
|
||||
] as const;
|
||||
|
||||
export const PROJECT_RUN_STATUSES = [
|
||||
"ready",
|
||||
"queued",
|
||||
"working",
|
||||
"needs-input",
|
||||
"completed",
|
||||
"failed",
|
||||
] as const;
|
||||
|
||||
export type ProjectRunStatus = (typeof PROJECT_RUN_STATUSES)[number];
|
||||
|
||||
export type ProjectWorkPhase =
|
||||
| "captured"
|
||||
| "queued"
|
||||
| "active"
|
||||
| "waiting"
|
||||
| "completed"
|
||||
| "failed";
|
||||
|
||||
export type ProjectVerificationStatus =
|
||||
| "not-started"
|
||||
| "running"
|
||||
| "blocked"
|
||||
| "passed"
|
||||
| "failed";
|
||||
|
||||
export interface ProjectWorkStatus {
|
||||
readonly label: string;
|
||||
readonly phase: ProjectWorkPhase;
|
||||
readonly verification: ProjectVerificationStatus;
|
||||
}
|
||||
|
||||
const STATUS_MAP: Readonly<
|
||||
Record<ProjectIssueStatus | ProjectRunStatus, ProjectWorkStatus>
|
||||
> = {
|
||||
completed: {
|
||||
label: "Completed",
|
||||
phase: "completed",
|
||||
verification: "passed",
|
||||
},
|
||||
failed: {
|
||||
label: "Failed",
|
||||
phase: "failed",
|
||||
verification: "failed",
|
||||
},
|
||||
"needs-input": {
|
||||
label: "Needs your input",
|
||||
phase: "waiting",
|
||||
verification: "blocked",
|
||||
},
|
||||
open: {
|
||||
label: "Ready to start",
|
||||
phase: "captured",
|
||||
verification: "not-started",
|
||||
},
|
||||
queued: {
|
||||
label: "Queued",
|
||||
phase: "queued",
|
||||
verification: "not-started",
|
||||
},
|
||||
ready: {
|
||||
label: "Ready",
|
||||
phase: "queued",
|
||||
verification: "not-started",
|
||||
},
|
||||
working: {
|
||||
label: "In progress",
|
||||
phase: "active",
|
||||
verification: "running",
|
||||
},
|
||||
};
|
||||
|
||||
export const getProjectWorkStatus = (
|
||||
status: ProjectIssueStatus | ProjectRunStatus
|
||||
): ProjectWorkStatus => STATUS_MAP[status];
|
||||
|
||||
export const getProjectWorkProgress = (
|
||||
status: ProjectIssueStatus | ProjectRunStatus
|
||||
): number => {
|
||||
switch (status) {
|
||||
case "completed": {
|
||||
return 100;
|
||||
}
|
||||
case "failed": {
|
||||
return 58;
|
||||
}
|
||||
case "needs-input": {
|
||||
return 78;
|
||||
}
|
||||
case "working": {
|
||||
return 68;
|
||||
}
|
||||
case "queued":
|
||||
case "ready": {
|
||||
return 24;
|
||||
}
|
||||
case "open": {
|
||||
return 8;
|
||||
}
|
||||
default: {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user