papilot 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -30
- package/README.zh-CN.md +58 -30
- package/bin/doctor.mjs +53 -6
- package/bin/installation.d.mts +8 -0
- package/bin/installation.mjs +54 -0
- package/bin/papilot.mjs +6 -2
- package/dist/assets/_virtual_papilot-licenses-D7ro6ecN.js +7597 -0
- package/dist/assets/index-Cc-awdNn.css +1 -0
- package/dist/assets/index-D_VX51no.js +265 -0
- package/dist/assets/latex-BarqJFtQ.svg +1 -0
- package/dist/assets/{math-DY-H5SfX.js → math-V9uUaUzA.js} +1 -1
- package/dist/assets/pdf-view-Bnb2uIBE.js +598 -0
- package/dist/index.html +2 -2
- package/examples/papilot-paper/main.tex +1 -1
- package/examples/papilot-paper/sections/02-using.tex +3 -1
- package/examples/papilot-paper/sections/09-agents.tex +8 -5
- package/package.json +15 -7
- package/scripts/service.ts +1 -0
- package/skills/ai-review.md +18 -0
- package/skills/latex-warnings.md +1 -1
- package/src/conversation.ts +6 -0
- package/src/note-groups.ts +30 -0
- package/src/notes.ts +34 -10
- package/src/server/acp.ts +354 -0
- package/src/server/agents.ts +237 -47
- package/src/server/ai-review-routes.ts +72 -0
- package/src/server/ai-review.ts +38 -0
- package/src/server/annotation-placement.ts +99 -0
- package/src/server/app.ts +216 -21
- package/src/server/attachments.ts +128 -0
- package/src/server/build-problems.ts +329 -0
- package/src/server/changes.ts +28 -2
- package/src/server/codex.ts +2 -1
- package/src/server/cursor.ts +153 -0
- package/src/server/diagnostics.ts +45 -0
- package/src/server/git-overview.ts +69 -0
- package/src/server/imports.ts +205 -0
- package/src/server/latex-installation.ts +43 -0
- package/src/server/main.ts +8 -42
- package/src/server/manuscript.ts +212 -12
- package/src/server/note-results.ts +65 -0
- package/src/server/paths.ts +1 -1
- package/src/server/references.ts +70 -0
- package/src/server/review.ts +6 -5
- package/src/server/skill-library.ts +113 -0
- package/src/server/skill-routes.ts +19 -0
- package/src/server/skills.ts +29 -19
- package/src/server/start.ts +51 -0
- package/src/server/store.ts +44 -4
- package/src/server/update-install.ts +124 -0
- package/src/server/update-worker.ts +78 -0
- package/src/server/updates.ts +152 -0
- package/src/server/workspace.ts +450 -60
- package/src/shared.ts +120 -10
- package/src/skill-icons.ts +18 -0
- package/src/skill-metadata.ts +45 -0
- package/src/task-caption.ts +20 -0
- package/vendor/latex-review/dist/web/assets/index-7VKAte5x.js +19 -0
- package/vendor/latex-review/dist/web/assets/index-DEz-LsGl.css +1 -0
- package/vendor/latex-review/dist/web/index.html +2 -2
- package/vendor/latex-review/src/core/bib.ts +692 -0
- package/vendor/latex-review/src/core/build.ts +5 -1
- package/vendor/latex-review/src/core/changes.ts +39 -0
- package/vendor/latex-review/src/core/delivery.ts +109 -0
- package/vendor/latex-review/src/core/review.ts +12 -4
- package/vendor/latex-review/src/core/store.ts +3 -0
- package/vendor/latex-review/src/core/tasks.ts +5 -2
- package/vendor/latex-review/src/core/verify.ts +784 -0
- package/vendor/latex-review/src/core/view.ts +204 -0
- package/vendor/latex-review/src/render/round.ts +11 -0
- package/vendor/latex-review/src/server.ts +81 -1
- package/vendor/latex-review/src/types.ts +141 -0
- package/vendor/latex-review/src/web/effect-preferences.ts +10 -0
- package/vendor/latex-review/src/web/i18n.ts +1 -1
- package/vendor/latex-review/src/web/overview.tsx +1 -5
- package/vendor/latex-review/src/web/references.tsx +499 -0
- package/vendor/latex-review/src/web/review.tsx +381 -230
- package/vendor/latex-review/src/web/shortcuts.ts +53 -0
- package/vendor/latex-review/src/web/style.css +295 -0
- package/vendor/latex-review/src/web/zh-CN.ts +95 -26
- package/dist/assets/index-BNGqA0_L.css +0 -1
- package/dist/assets/index-Bt9UXsGX.js +0 -222
- package/dist/assets/pdf-view-BGp3W-RG.js +0 -598
- package/vendor/latex-review/dist/web/assets/index-AFBj4gQ9.js +0 -19
- package/vendor/latex-review/dist/web/assets/index-DfC0GURl.css +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Repo } from "./git.ts";
|
|
6
6
|
import { listFiles, readJson, writeJson, withLock, now } from "./files.ts";
|
|
@@ -197,6 +197,7 @@ export class Builder {
|
|
|
197
197
|
rev: string,
|
|
198
198
|
targets = ["full"],
|
|
199
199
|
seed: BuildInfo | null = null,
|
|
200
|
+
options: { force?: boolean } = {},
|
|
200
201
|
): Promise<BuildInfo> {
|
|
201
202
|
const p = this.project,
|
|
202
203
|
partial = JSON.stringify(targets) !== '["full"]';
|
|
@@ -223,6 +224,9 @@ export class Builder {
|
|
|
223
224
|
version,
|
|
224
225
|
date,
|
|
225
226
|
hash(fs.readFileSync(fileURLToPath(import.meta.url))),
|
|
227
|
+
// Manual recompilation gets a fresh output directory. Other previews
|
|
228
|
+
// and review rounds may still be reading the previous cached PDF.
|
|
229
|
+
...(options.force ? [randomUUID()] : []),
|
|
226
230
|
]),
|
|
227
231
|
).slice(0, 32),
|
|
228
232
|
folder = path.join(this.cache, key);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Repo, within } from "./git.ts";
|
|
2
2
|
import { groups, opcodes } from "./diff.ts";
|
|
3
|
+
import { entrySpans } from "./bib.ts";
|
|
3
4
|
import type { Scope, Unit, Op } from "../types.ts";
|
|
4
5
|
export const EXCERPT = 6000;
|
|
5
6
|
export function textOf(data: Buffer | null): string | null {
|
|
@@ -34,6 +35,8 @@ export function diffUnits(
|
|
|
34
35
|
const left = binary ? [] : splitLines(a!),
|
|
35
36
|
right = binary ? [] : splitLines(b!);
|
|
36
37
|
let hunks = binary ? [] : groups(opcodes(left, right));
|
|
38
|
+
if (!binary && file.endsWith(".bib"))
|
|
39
|
+
hunks = byEntry(hunks, entrySpans(a!), entrySpans(b!));
|
|
37
40
|
const whole =
|
|
38
41
|
binary || status !== "modified" || isFigure(file) || !hunks.length;
|
|
39
42
|
if (whole) hunks = [[["replace", 0, left.length, 0, right.length]]];
|
|
@@ -61,6 +64,42 @@ export function diffUnits(
|
|
|
61
64
|
}
|
|
62
65
|
return units;
|
|
63
66
|
}
|
|
67
|
+
// A bibliography entry is decided whole: hunks that touch the same entry,
|
|
68
|
+
// before or after, or the same key on either side, become one change, so
|
|
69
|
+
// accepting part of an entry cannot mix its old and new fields.
|
|
70
|
+
export function byEntry(
|
|
71
|
+
hunks: Op[][],
|
|
72
|
+
before: { key: string; span: [number, number] }[],
|
|
73
|
+
after: { key: string; span: [number, number] }[],
|
|
74
|
+
) {
|
|
75
|
+
// An empty range touches an entry it falls strictly inside.
|
|
76
|
+
const touches = ([a, b]: [number, number], [s, e]: [number, number]) =>
|
|
77
|
+
a === b ? s < a && a < e : a < e && s < b;
|
|
78
|
+
const root = hunks.map((_, i) => i),
|
|
79
|
+
find = (i: number): number => (root[i] === i ? i : (root[i] = find(root[i]))),
|
|
80
|
+
owner = new Map<string, number>();
|
|
81
|
+
hunks.forEach((hunk, i) => {
|
|
82
|
+
const old: [number, number] = [hunk[0][1], hunk.at(-1)![2]],
|
|
83
|
+
next: [number, number] = [hunk[0][3], hunk.at(-1)![4]],
|
|
84
|
+
keys = [
|
|
85
|
+
...before.filter((e) => touches(old, e.span)).map((e) => e.key),
|
|
86
|
+
...after.filter((e) => touches(next, e.span)).map((e) => e.key),
|
|
87
|
+
];
|
|
88
|
+
for (const key of keys) {
|
|
89
|
+
const j = owner.get(key);
|
|
90
|
+
if (j === undefined) owner.set(key, i);
|
|
91
|
+
else root[find(i)] = find(j);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const merged = new Map<number, Op[]>();
|
|
95
|
+
hunks.forEach((hunk, i) => {
|
|
96
|
+
const r = find(i);
|
|
97
|
+
merged.set(r, [...(merged.get(r) ?? []), ...hunk]);
|
|
98
|
+
});
|
|
99
|
+
return [...merged.values()].map((ops) =>
|
|
100
|
+
ops.sort((p, q) => p[1] - q[1] || p[3] - q[3]),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
64
103
|
function edits(units: Unit[], index: 1 | 3): Op[] {
|
|
65
104
|
return units.flatMap((u) => u.ops).sort((a, b) => b[index] - a[index]);
|
|
66
105
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { App } from "../server.ts";
|
|
4
|
+
import type { Delivery } from "../types.ts";
|
|
5
|
+
import { merge3 } from "./git.ts";
|
|
6
|
+
import { readBytes, same, writeOrDelete } from "./files.ts";
|
|
7
|
+
import { safePath } from "./review.ts";
|
|
8
|
+
|
|
9
|
+
const encode = (data: Buffer | null) => data?.toString("base64") ?? null;
|
|
10
|
+
const decode = (data: string | null) => data === null ? null : Buffer.from(data, "base64");
|
|
11
|
+
|
|
12
|
+
// Acceptance stays in Git objects until the author chooses a destination.
|
|
13
|
+
// Persist the exact intended result before touching files or a branch so a
|
|
14
|
+
// lost response or a restart can finish the same operation without duplication.
|
|
15
|
+
export class Deliveries {
|
|
16
|
+
constructor(public app: App) {}
|
|
17
|
+
get(name: string) {
|
|
18
|
+
return this.app.store.one<{ data: Delivery }>("SELECT data FROM deliveries WHERE task=?", name)?.data;
|
|
19
|
+
}
|
|
20
|
+
save(name: string, data: Delivery) {
|
|
21
|
+
this.app.store.execute("INSERT OR REPLACE INTO deliveries(task,data) VALUES(?,?)", name, JSON.stringify(data));
|
|
22
|
+
}
|
|
23
|
+
assertReady(name: string) {
|
|
24
|
+
const last = this.get(name);
|
|
25
|
+
if (last && !last.done) throw new Error("Finish the pending delivery before changing this review. Retry the same destination.");
|
|
26
|
+
}
|
|
27
|
+
deliver(name: string, mode: Delivery["mode"], message = "") {
|
|
28
|
+
const app = this.app, task = app.tasks.get(name), last = this.get(name);
|
|
29
|
+
if (!task.deferred) throw new Error("This task already applies accepted changes to the manuscript.");
|
|
30
|
+
if (mode !== "workspace" && mode !== "branch") throw new Error("Choose a working directory or task branch.");
|
|
31
|
+
if (last && last.mode !== mode) throw new Error("Continue using this task's original destination, or start a new task.");
|
|
32
|
+
if (last && (!last.done || last.accepted === task.accepted)) return this.finish(name, last);
|
|
33
|
+
if (app.publisher.busy(name) || app.review.counts(name).pending) throw new Error("Finish reviewing every change before choosing a destination.");
|
|
34
|
+
const before = last?.accepted ?? task.base;
|
|
35
|
+
const files = app.repo.changedFiles(before, task.accepted);
|
|
36
|
+
if (!files.length) throw new Error("This task has no accepted changes left to deliver.");
|
|
37
|
+
const base = app.repo.blobs(before, files), accepted = app.repo.blobs(task.accepted, files);
|
|
38
|
+
const data: Delivery = { mode, accepted: task.accepted, seq: app.store.one<{ seq: number }>("SELECT MAX(seq) AS seq FROM actions WHERE task=?", name)!.seq, done: false, writes: [] };
|
|
39
|
+
const contents: Record<string, Buffer | null> = {};
|
|
40
|
+
let current: Record<string, Buffer | null>;
|
|
41
|
+
if (mode === "branch") {
|
|
42
|
+
if (!message.trim() || message.length > 5000) throw new Error("Write a commit message (at most 5,000 characters).");
|
|
43
|
+
data.branch = `papilot/${name}`;
|
|
44
|
+
data.previous = last?.commit;
|
|
45
|
+
// The base snapshot may include the author's uncommitted edits. Apply
|
|
46
|
+
// only the accepted delta onto its original HEAD, never commit those edits.
|
|
47
|
+
data.parent = last?.commit ?? app.repo.rev(`${task.base}^`);
|
|
48
|
+
current = app.repo.blobs(data.parent, files);
|
|
49
|
+
const ref = app.repo.text(["show-ref", "--verify", "--hash", `refs/heads/${data.branch}`], { check: false });
|
|
50
|
+
if (ref !== (data.previous ?? "")) throw new Error("The task branch was changed outside Papilot. Nothing was committed.");
|
|
51
|
+
this.assertBranchAvailable(data.branch);
|
|
52
|
+
} else {
|
|
53
|
+
this.assertNoMerge();
|
|
54
|
+
current = Object.fromEntries(files.map(file => [file, readBytes(safePath(app.root, file))]));
|
|
55
|
+
}
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const [merged, conflict] = merge3(current[file], base[file], accepted[file]);
|
|
58
|
+
if (conflict) throw new Error(`Cannot deliver ${file}: it conflicts with the destination. Nothing was written. Revise the task and try again.`);
|
|
59
|
+
contents[file] = merged;
|
|
60
|
+
if (mode === "workspace" && !same(current[file], merged)) data.writes.push({ file, before: encode(current[file]), after: encode(merged) });
|
|
61
|
+
}
|
|
62
|
+
if (mode === "branch") {
|
|
63
|
+
const tree = app.repo.treeWith(data.parent!, contents);
|
|
64
|
+
data.commit = app.repo.text(["commit-tree", tree, "-p", data.parent!, "-m", message.trim()]);
|
|
65
|
+
}
|
|
66
|
+
this.save(name, data);
|
|
67
|
+
return this.finish(name, data);
|
|
68
|
+
}
|
|
69
|
+
private assertBranchAvailable(branch: string) {
|
|
70
|
+
if (this.app.repo.text(["worktree", "list", "--porcelain"]).split("\n").includes(`branch refs/heads/${branch}`))
|
|
71
|
+
throw new Error("The task branch is checked out. Switch that worktree to another branch before committing here.");
|
|
72
|
+
}
|
|
73
|
+
private assertNoMerge() {
|
|
74
|
+
if (fs.existsSync(path.resolve(this.app.root, this.app.repo.text(["rev-parse", "--git-path", "MERGE_HEAD"]))))
|
|
75
|
+
throw new Error("Finish or abort the merge before applying changes.");
|
|
76
|
+
}
|
|
77
|
+
finish(name: string, data: Delivery) {
|
|
78
|
+
if (data.done) return this.result(data);
|
|
79
|
+
const app = this.app;
|
|
80
|
+
if (data.mode === "branch") {
|
|
81
|
+
const ref = `refs/heads/${data.branch}`;
|
|
82
|
+
const current = app.repo.text(["show-ref", "--verify", "--hash", ref], { check: false });
|
|
83
|
+
if (current !== data.commit) {
|
|
84
|
+
this.assertBranchAvailable(data.branch!);
|
|
85
|
+
app.repo.run(["update-ref", ref, data.commit!, data.previous ?? "0".repeat(data.commit!.length)]);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
this.assertNoMerge();
|
|
89
|
+
const writes = data.writes.map(write => ({ ...write, target: safePath(app.root, write.file) }));
|
|
90
|
+
for (const write of writes) {
|
|
91
|
+
const current = readBytes(write.target);
|
|
92
|
+
if (!same(current, decode(write.before)) && !same(current, decode(write.after)))
|
|
93
|
+
throw new Error(`Cannot finish delivery: ${write.file} changed. Restore it to its state before delivery and retry.`);
|
|
94
|
+
}
|
|
95
|
+
for (const write of writes) writeOrDelete(write.target, decode(write.after));
|
|
96
|
+
}
|
|
97
|
+
this.save(name, { ...data, done: true, writes: [] });
|
|
98
|
+
return this.result(data);
|
|
99
|
+
}
|
|
100
|
+
result(data: Delivery) {
|
|
101
|
+
const { mode, accepted, branch, commit } = data;
|
|
102
|
+
return { mode, accepted, ...(branch ? { branch, commit } : {}) };
|
|
103
|
+
}
|
|
104
|
+
recover() {
|
|
105
|
+
for (const row of this.app.store.all<{ task: string; data: Delivery }>("SELECT * FROM deliveries")) {
|
|
106
|
+
if (!row.data.done) try { this.finish(row.task, row.data); } catch { /* Retain the journal for an explicit retry. */ }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -121,7 +121,8 @@ export class Review {
|
|
|
121
121
|
"SELECT * FROM actions WHERE task=? AND status='applied' ORDER BY seq DESC LIMIT 1",
|
|
122
122
|
name,
|
|
123
123
|
);
|
|
124
|
-
|
|
124
|
+
const delivered = this.app.delivery.get(name);
|
|
125
|
+
return action && latest && action.round === latest.number && (!delivered || action.seq > delivered.seq) ? action : null;
|
|
125
126
|
}
|
|
126
127
|
summary(name: string): Summary {
|
|
127
128
|
const task = this.app.tasks.get(name),
|
|
@@ -199,6 +200,10 @@ export class Review {
|
|
|
199
200
|
closed: Boolean(task.closed),
|
|
200
201
|
state: task.closed ? null : this.summary(name).state,
|
|
201
202
|
latest: latest?.number ?? null,
|
|
203
|
+
history: this.app.store.all<{ number: number; created: number }>(
|
|
204
|
+
"SELECT number,created FROM rounds WHERE task=? ORDER BY number DESC",
|
|
205
|
+
name,
|
|
206
|
+
),
|
|
202
207
|
round: number ?? null,
|
|
203
208
|
revision: round?.after ?? null,
|
|
204
209
|
decisions: round ? this.decisions(name, round) : {},
|
|
@@ -256,6 +261,7 @@ export class Review {
|
|
|
256
261
|
loose: render.loose,
|
|
257
262
|
warnings: round.summary.warnings ?? [],
|
|
258
263
|
files: `/files/${name}/${number}/`,
|
|
264
|
+
...(render.references ? { references: render.references } : {}),
|
|
259
265
|
};
|
|
260
266
|
}
|
|
261
267
|
history(name: string, round: Round): Record<string, History[]> {
|
|
@@ -360,6 +366,7 @@ export class Review {
|
|
|
360
366
|
) {
|
|
361
367
|
const task = this.app.tasks.get(name),
|
|
362
368
|
kind = data.action;
|
|
369
|
+
this.app.delivery.assertReady(name);
|
|
363
370
|
if (!Object.hasOwn(DECIDED, kind))
|
|
364
371
|
throw new Error("The action must be accept, reject or comment.");
|
|
365
372
|
const identity = data.id || randomUUID();
|
|
@@ -440,8 +447,8 @@ export class Review {
|
|
|
440
447
|
records = app.repo.blobs(task.accepted, files);
|
|
441
448
|
for (const [file, group] of Object.entries(groups)) {
|
|
442
449
|
const proposed = applyUnits(before[file], after[file], group),
|
|
443
|
-
target = safePath(app.root, file),
|
|
444
|
-
current = readBytes(target),
|
|
450
|
+
target = task.deferred ? "" : safePath(app.root, file),
|
|
451
|
+
current = task.deferred ? records[file] : readBytes(target),
|
|
445
452
|
[merged, conflict] = merge3(current, before[file], proposed),
|
|
446
453
|
[recorded, recordedConflict] = merge3(
|
|
447
454
|
records[file],
|
|
@@ -449,7 +456,7 @@ export class Review {
|
|
|
449
456
|
proposed,
|
|
450
457
|
);
|
|
451
458
|
if (conflict || recordedConflict) conflicts.push(file);
|
|
452
|
-
else if (!same(merged, current)) writes.push([target, current, merged]);
|
|
459
|
+
else if (!task.deferred && !same(merged, current)) writes.push([target, current, merged]);
|
|
453
460
|
accepted[file] = recorded;
|
|
454
461
|
}
|
|
455
462
|
if (conflicts.length)
|
|
@@ -672,6 +679,7 @@ export class Review {
|
|
|
672
679
|
return this.pageState(name);
|
|
673
680
|
}
|
|
674
681
|
undo(name: string, identity: string) {
|
|
682
|
+
this.app.delivery.assertReady(name);
|
|
675
683
|
const app = this.app,
|
|
676
684
|
task = app.tasks.get(name),
|
|
677
685
|
action = this.undoEntry(name);
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
const SCHEMA = `
|
|
5
5
|
CREATE TABLE IF NOT EXISTS tasks(name TEXT PRIMARY KEY,title TEXT NOT NULL,worktree TEXT NOT NULL,base TEXT NOT NULL,accepted TEXT NOT NULL,scope TEXT NOT NULL,created REAL NOT NULL,closed REAL);
|
|
6
|
+
CREATE TABLE IF NOT EXISTS deliveries(task TEXT PRIMARY KEY,data TEXT NOT NULL);
|
|
6
7
|
CREATE TABLE IF NOT EXISTS rounds(task TEXT NOT NULL,number INTEGER NOT NULL,before TEXT NOT NULL,after TEXT NOT NULL,units TEXT NOT NULL,summary TEXT NOT NULL,created REAL NOT NULL,PRIMARY KEY(task,number));
|
|
7
8
|
CREATE TABLE IF NOT EXISTS actions(seq INTEGER PRIMARY KEY AUTOINCREMENT,id TEXT NOT NULL UNIQUE,task TEXT NOT NULL,round INTEGER NOT NULL,kind TEXT NOT NULL,changes TEXT NOT NULL,body TEXT NOT NULL DEFAULT '',status TEXT NOT NULL,detail TEXT NOT NULL DEFAULT '{}',created REAL NOT NULL);
|
|
8
9
|
CREATE TABLE IF NOT EXISTS writes(action TEXT NOT NULL,phase TEXT NOT NULL,path TEXT NOT NULL,expected BLOB,target BLOB,PRIMARY KEY(action,phase,path));
|
|
@@ -36,6 +37,8 @@ export class Store {
|
|
|
36
37
|
this.db = new DatabaseSync(file);
|
|
37
38
|
this.db.exec("PRAGMA busy_timeout=30000; PRAGMA journal_mode=WAL;");
|
|
38
39
|
this.db.exec(SCHEMA);
|
|
40
|
+
if (!this.all<{ name: string }>("PRAGMA table_info(tasks)").some(column => column.name === "deferred"))
|
|
41
|
+
this.db.exec("ALTER TABLE tasks ADD COLUMN deferred INTEGER NOT NULL DEFAULT 0");
|
|
39
42
|
}
|
|
40
43
|
transaction<T>(work: () => T): T {
|
|
41
44
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -78,6 +78,7 @@ export class Tasks {
|
|
|
78
78
|
title?: string,
|
|
79
79
|
sections: string[] = [],
|
|
80
80
|
files: string[] = [],
|
|
81
|
+
deferred = false,
|
|
81
82
|
): Task {
|
|
82
83
|
const { app } = this,
|
|
83
84
|
project = app.project;
|
|
@@ -154,10 +155,10 @@ export class Tasks {
|
|
|
154
155
|
name,
|
|
155
156
|
);
|
|
156
157
|
app.store.execute("DELETE FROM tasks WHERE name=?", name);
|
|
157
|
-
for (const table of ["rounds", "actions", "messages", "jobs"])
|
|
158
|
+
for (const table of ["rounds", "actions", "messages", "jobs", "deliveries"])
|
|
158
159
|
app.store.execute(`DELETE FROM ${table} WHERE task=?`, name);
|
|
159
160
|
app.store.execute(
|
|
160
|
-
"INSERT INTO tasks(name,title,worktree,base,accepted,scope,created) VALUES(
|
|
161
|
+
"INSERT INTO tasks(name,title,worktree,base,accepted,scope,created,deferred) VALUES(?,?,?,?,?,?,?,?)",
|
|
161
162
|
name,
|
|
162
163
|
title,
|
|
163
164
|
worktree,
|
|
@@ -165,6 +166,7 @@ export class Tasks {
|
|
|
165
166
|
base,
|
|
166
167
|
JSON.stringify(scope),
|
|
167
168
|
now(),
|
|
169
|
+
Number(deferred),
|
|
168
170
|
);
|
|
169
171
|
});
|
|
170
172
|
} catch (error) {
|
|
@@ -196,6 +198,7 @@ export class Tasks {
|
|
|
196
198
|
);
|
|
197
199
|
}
|
|
198
200
|
async finish(name: string, force = false) {
|
|
201
|
+
this.app.delivery.assertReady(name);
|
|
199
202
|
const app = this.app,
|
|
200
203
|
task = this.get(name);
|
|
201
204
|
if (app.publisher.busy(name))
|