Default recurring task exports to checked

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-23 12:42:20 -05:00
parent c41dd2e393
commit 220946b2a1
3 changed files with 105 additions and 13 deletions

View file

@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { buildInitialExportCheckedFiles } from "./company-export-selection";
describe("buildInitialExportCheckedFiles", () => {
it("checks non-task files and recurring task packages by default", () => {
const checked = buildInitialExportCheckedFiles(
[
"README.md",
".paperclip.yaml",
"tasks/one-off/TASK.md",
"tasks/recurring/TASK.md",
"tasks/recurring/notes.md",
],
[
{ path: "tasks/one-off/TASK.md", recurring: false },
{ path: "tasks/recurring/TASK.md", recurring: true },
],
new Set<string>(),
);
expect(Array.from(checked).sort()).toEqual([
".paperclip.yaml",
"README.md",
"tasks/recurring/TASK.md",
"tasks/recurring/notes.md",
]);
});
it("preserves previous manual selections for one-time tasks", () => {
const checked = buildInitialExportCheckedFiles(
["README.md", "tasks/one-off/TASK.md"],
[{ path: "tasks/one-off/TASK.md", recurring: false }],
new Set(["tasks/one-off/TASK.md"]),
);
expect(Array.from(checked).sort()).toEqual([
"README.md",
"tasks/one-off/TASK.md",
]);
});
});

View file

@ -0,0 +1,56 @@
import type { CompanyPortabilityIssueManifestEntry } from "@paperclipai/shared";
function isTaskPath(filePath: string): boolean {
return /(?:^|\/)tasks\//.test(filePath);
}
function buildRecurringTaskPrefixes(
issues: Array<Pick<CompanyPortabilityIssueManifestEntry, "path" | "recurring">>,
): Set<string> {
const prefixes = new Set<string>();
for (const issue of issues) {
if (!issue.recurring) continue;
const filePath = issue.path.trim();
if (!filePath) continue;
prefixes.add(filePath);
const lastSlash = filePath.lastIndexOf("/");
if (lastSlash >= 0) {
prefixes.add(`${filePath.slice(0, lastSlash + 1)}`);
}
}
return prefixes;
}
function isRecurringTaskFile(filePath: string, recurringTaskPrefixes: Set<string>): boolean {
for (const prefix of recurringTaskPrefixes) {
if (filePath === prefix || filePath.startsWith(prefix)) return true;
}
return false;
}
export function buildInitialExportCheckedFiles(
filePaths: string[],
issues: Array<Pick<CompanyPortabilityIssueManifestEntry, "path" | "recurring">>,
previousCheckedFiles: Set<string>,
): Set<string> {
const next = new Set<string>();
const recurringTaskPrefixes = buildRecurringTaskPrefixes(issues);
for (const filePath of filePaths) {
if (previousCheckedFiles.has(filePath)) {
next.add(filePath);
continue;
}
if (!isTaskPath(filePath) || isRecurringTaskFile(filePath, recurringTaskPrefixes)) {
next.add(filePath);
}
}
return next;
}

View file

@ -17,6 +17,7 @@ import { PageSkeleton } from "../components/PageSkeleton";
import { MarkdownBody } from "../components/MarkdownBody";
import { cn } from "../lib/utils";
import { createZipArchive } from "../lib/zip";
import { buildInitialExportCheckedFiles } from "../lib/company-export-selection";
import { getPortableFileDataUrl, getPortableFileText, isPortableImageFile } from "../lib/portable-files";
import {
Download,
@ -34,11 +35,6 @@ import {
PackageFileTree,
} from "../components/PackageFileTree";
/** Returns true if the path looks like a task file (e.g. tasks/slug/TASK.md or projects/x/tasks/slug/TASK.md) */
function isTaskPath(filePath: string): boolean {
return /(?:^|\/)tasks\//.test(filePath);
}
/**
* Extract the set of agent/project/task slugs that are "checked" based on
* which file paths are in the checked set.
@ -588,14 +584,13 @@ export function CompanyExport() {
}),
onSuccess: (result) => {
setExportData(result);
setCheckedFiles((prev) => {
const next = new Set<string>();
for (const filePath of Object.keys(result.files)) {
if (prev.has(filePath)) next.add(filePath);
else if (!isTaskPath(filePath)) next.add(filePath);
}
return next;
});
setCheckedFiles((prev) =>
buildInitialExportCheckedFiles(
Object.keys(result.files),
result.manifest.issues,
prev,
),
);
// Expand top-level dirs (except tasks — collapsed by default)
const tree = buildFileTree(result.files);
const topDirs = new Set<string>();