Files
PaperClipAI/packages/shared/src/validators/project.ts
T
eaef47f4c7 Information Architecture + project/agent visual refresh (experimental) (#7543)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The board UI is the control surface for issues, projects, agents,
goals, workspaces, and operator settings.
> - The existing navigation and list surfaces make several
high-frequency workflows feel harder to scan than they should,
especially around projects and agents.
> - The product direction is to improve those surfaces without breaking
the existing route model or forcing a new IA on every operator at once.
> - This pull request now keeps the dependent IA, project identity, and
agent-list visual refresh work together while the Issue-to-Task copy
migration is split into #7651.
> - The benefit is a clearer left nav, better project identity, denser
agent/project list rows, and brand-aligned status treatment while
preserving the classic default experience behind a flag.

## Linked Issues or Issue Description

Refs #7645
Refs #7651

Internal planning/work references: PAP-53, PAP-56, PAP-58, PAP-59,
PAP-60, PAP-61, PAP-68, PAP-69, PAP-70, PAP-71, PAP-72, PAP-75, PAP-76,
PAP-80, PAP-85, PAP-86, PAP-87, PAP-88, PAP-89.

## What Changed

- Adds `enableStreamlinedLeftNavigation`, defaulting off, and gates
sidebar presentation so classic navigation remains the default.
- Adds project icon persistence, validation, portability, picker UI, and
`ProjectTile` rendering while defaulting new projects to neutral gray.
- Adds projects-list task-count and budget summary data with focused
server/shared/UI coverage.
- Refreshes agent list rows, row actions, active/recent sidebar
behavior, and status capsule/chip styling for the approved brand state
system.
- Removes the placeholder Conference room and Artifacts nav/routes from
the finalized experimental nav direction.
- Removes `pnpm-lock.yaml` and the Issue-to-Task copy migration from
this PR diff; the copy migration now lives in #7651.

## Verification

- Existing branch verification from the authored commits: UI typecheck,
targeted unit tests, and light/dark visual checks for `/agents`, agent
detail, and design-guide status states.
- Maintainer cleanup verification on `75e34e5`: `git diff --check
origin/master...HEAD` passed, the `design/` diff is empty, and the PR
diff is 61 files, below Greptile's 100-file review limit.
- `pnpm --filter @paperclipai/ui build` passed.
- `NODE_ENV=test pnpm exec vitest run
ui/src/components/Sidebar.test.tsx` passed: 1 file, 8 tests.
- CI and Greptile should rerun on the latest push.

## Risks

- Broad UI surface area: the experimental flag keeps the classic nav
default, but changed shared components such as `EntityRow`,
`ProjectTile`, and agent status badges could affect multiple pages.
- Database migration: `projects.icon` is additive and nullable, but
migration ordering and portability import/export must stay aligned.
- The Issue-to-Task copy migration is now separated into #7651, so
reviewers should evaluate this PR as IA/project/agent presentation work
only.
- Visual regressions are possible across smaller widths because the PR
intentionally changes dense list-row layouts.

## Model Used

Claude Opus 4.8 assisted the original feature commits.
Paperclip-Paperclip agents assisted some planning/design commits. Codex
/ GPT-5-class coding agent with shell, GitHub CLI, and repository access
performed this PR-readiness cleanup and split.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Dotta <bippadotta@protonmail.com>
2026-06-06 09:17:27 -05:00

128 lines
5.3 KiB
TypeScript

import { z } from "zod";
import { PROJECT_STATUSES, PROJECT_ICON_NAMES } from "../constants.js";
import { envConfigSchema } from "./secret.js";
import { trustAuthorizationPolicySchema } from "./trust-policy.js";
const executionWorkspaceStrategySchema = z
.object({
type: z.enum(["project_primary", "git_worktree", "adapter_managed", "cloud_sandbox"]).optional(),
baseRef: z.string().optional().nullable(),
branchTemplate: z.string().optional().nullable(),
worktreeParentDir: z.string().optional().nullable(),
provisionCommand: z.string().optional().nullable(),
teardownCommand: z.string().optional().nullable(),
})
.strict();
export const projectExecutionWorkspacePolicySchema = z
.object({
enabled: z.boolean(),
defaultMode: z.enum(["shared_workspace", "isolated_workspace", "operator_branch", "adapter_default"]).optional(),
allowIssueOverride: z.boolean().optional(),
defaultProjectWorkspaceId: z.string().uuid().optional().nullable(),
environmentId: z.string().uuid().optional().nullable(),
workspaceStrategy: executionWorkspaceStrategySchema.optional().nullable(),
workspaceRuntime: z.record(z.string(), z.unknown()).optional().nullable(),
branchPolicy: z.record(z.string(), z.unknown()).optional().nullable(),
pullRequestPolicy: z.record(z.string(), z.unknown()).optional().nullable(),
runtimePolicy: z.record(z.string(), z.unknown()).optional().nullable(),
cleanupPolicy: z.record(z.string(), z.unknown()).optional().nullable(),
authorizationPolicy: trustAuthorizationPolicySchema.optional().nullable(),
})
.strict();
export const projectWorkspaceRuntimeConfigSchema = z.object({
workspaceRuntime: z.record(z.string(), z.unknown()).optional().nullable(),
desiredState: z.enum(["running", "stopped", "manual"]).optional().nullable(),
serviceStates: z.record(z.enum(["running", "stopped", "manual"])).optional().nullable(),
}).strict();
const projectWorkspaceSourceTypeSchema = z.enum(["local_path", "git_repo", "remote_managed", "non_git_path"]);
const projectWorkspaceVisibilitySchema = z.enum(["default", "advanced"]);
const projectWorkspaceFields = {
name: z.string().min(1).optional(),
sourceType: projectWorkspaceSourceTypeSchema.optional(),
cwd: z.string().min(1).optional().nullable(),
repoUrl: z.string().url().optional().nullable(),
repoRef: z.string().optional().nullable(),
defaultRef: z.string().optional().nullable(),
visibility: projectWorkspaceVisibilitySchema.optional(),
setupCommand: z.string().optional().nullable(),
cleanupCommand: z.string().optional().nullable(),
remoteProvider: z.string().optional().nullable(),
remoteWorkspaceRef: z.string().optional().nullable(),
sharedWorkspaceKey: z.string().optional().nullable(),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
runtimeConfig: projectWorkspaceRuntimeConfigSchema.optional().nullable(),
};
function validateProjectWorkspace(value: Record<string, unknown>, ctx: z.RefinementCtx) {
const sourceType = value.sourceType ?? "local_path";
const hasCwd = typeof value.cwd === "string" && value.cwd.trim().length > 0;
const hasRepo = typeof value.repoUrl === "string" && value.repoUrl.trim().length > 0;
const hasRemoteRef = typeof value.remoteWorkspaceRef === "string" && value.remoteWorkspaceRef.trim().length > 0;
if (sourceType === "remote_managed") {
if (!hasRemoteRef && !hasRepo) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Remote-managed workspace requires remoteWorkspaceRef or repoUrl.",
path: ["remoteWorkspaceRef"],
});
}
return;
}
if (!hasCwd && !hasRepo) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Workspace requires at least one of cwd or repoUrl.",
path: ["cwd"],
});
}
}
export const createProjectWorkspaceSchema = z.object({
...projectWorkspaceFields,
isPrimary: z.boolean().optional().default(false),
}).superRefine(validateProjectWorkspace);
export type CreateProjectWorkspace = z.infer<typeof createProjectWorkspaceSchema>;
export const updateProjectWorkspaceSchema = z.object({
...projectWorkspaceFields,
isPrimary: z.boolean().optional(),
}).partial();
export type UpdateProjectWorkspace = z.infer<typeof updateProjectWorkspaceSchema>;
const projectFields = {
/** @deprecated Use goalIds instead */
goalId: z.string().uuid().optional().nullable(),
goalIds: z.array(z.string().uuid()).optional(),
name: z.string().min(1),
description: z.string().optional().nullable(),
status: z.enum(PROJECT_STATUSES).optional().default("backlog"),
leadAgentId: z.string().uuid().optional().nullable(),
targetDate: z.string().optional().nullable(),
color: z.string().optional().nullable(),
icon: z.enum(PROJECT_ICON_NAMES).optional().nullable(),
env: envConfigSchema.optional().nullable(),
executionWorkspacePolicy: projectExecutionWorkspacePolicySchema.optional().nullable(),
archivedAt: z.string().datetime().optional().nullable(),
};
export const createProjectSchema = z.object({
...projectFields,
workspace: createProjectWorkspaceSchema.optional(),
});
export type CreateProject = z.infer<typeof createProjectSchema>;
export const updateProjectSchema = z.object(projectFields).partial();
export type UpdateProject = z.infer<typeof updateProjectSchema>;
export type ProjectExecutionWorkspacePolicy = z.infer<typeof projectExecutionWorkspacePolicySchema>;