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:
-Puter
2026-07-24 02:34:01 +05:30
10 changed files with 1347 additions and 295 deletions

View File

@@ -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": {

View File

@@ -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";

View 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")
);
});
});

View 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;
}
}
};